3 commits - plugins/calendar plugins/libcalendaring
Thomas Brüderli
bruederli at kolabsys.com
Thu Jul 18 12:41:06 CEST 2013
plugins/calendar/calendar.php | 3
plugins/calendar/lib/Horde_Date.php | 1305 -------
plugins/calendar/lib/Horde_iCalendar.php | 3300 -------------------
plugins/calendar/lib/calendar_ical.php | 569 ---
plugins/calendar/lib/get_horde_icalendar.sh | 31
plugins/libcalendaring/get_horde_icalendar.sh | 31
plugins/libcalendaring/lib/Horde_Date.php | 1304 +++++++
plugins/libcalendaring/lib/Horde_iCalendar.php | 3300 +++++++++++++++++++
plugins/libcalendaring/libcalendaring.php | 13
plugins/libcalendaring/libvcalendar.php | 575 +++
plugins/libcalendaring/tests/libvcalendar.php | 267 +
plugins/libcalendaring/tests/resources/invalid.txt | 2
plugins/libcalendaring/tests/resources/itip.ics | 149
plugins/libcalendaring/tests/resources/multiple.ics | 51
plugins/libcalendaring/tests/resources/recurring.ics | 43
plugins/libcalendaring/tests/resources/snd.ics | 18
16 files changed, 5753 insertions(+), 5208 deletions(-)
New commits:
commit af49088b38a7861fe940a4ad4883364024069418
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 18 12:40:46 2013 +0200
Add unit tests for ical import/export
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
new file mode 100644
index 0000000..b28c7ac
--- /dev/null
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -0,0 +1,267 @@
+<?php
+
+/**
+ * libcalendaring plugin's iCalendar functions tests
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class libvcalendar_test extends PHPUnit_Framework_TestCase
+{
+ function setUp()
+ {
+ require_once __DIR__ . '/../libvcalendar.php';
+ require_once __DIR__ . '/../libcalendaring.php';
+ }
+
+ /**
+ * Simple iCal parsing test
+ */
+ function test_import()
+ {
+ $ical = new libvcalendar();
+ $ics = file_get_contents(__DIR__ . '/resources/snd.ics');
+ $events = $ical->import($ics, 'UTF-8');
+
+ $this->assertEquals(1, count($events));
+ $event = $events[0];
+
+ $this->assertInstanceOf('DateTime', $event['start'], "'start' property is DateTime object");
+ $this->assertInstanceOf('DateTime', $event['end'], "'end' property is DateTime object");
+ $this->assertEquals('08-01', $event['start']->format('m-d'), "Start date is August 1st");
+ $this->assertTrue($event['allday'], "All-day event flag");
+
+ $this->assertEquals('B968B885-08FB-40E5-B89E-6DA05F26AA79', $event['uid'], "Event UID");
+ $this->assertEquals('Swiss National Day', $event['title'], "Event title");
+ $this->assertEquals('http://en.wikipedia.org/wiki/Swiss_National_Day', $event['url'], "URL property");
+ $this->assertEquals(2, $event['sequence'], "Sequence number");
+
+ $desclines = explode("\n", $event['description']);
+ $this->assertEquals(4, count($desclines), "Multiline description");
+ $this->assertEquals("French: Fête nationale Suisse", rtrim($desclines[1]), "UTF-8 encoding");
+ }
+
+ /**
+ * Test parsing from files
+ */
+ function test_import_from_file()
+ {
+ $ical = new libvcalendar();
+
+ $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
+ $this->assertEquals(2, count($events));
+
+ $events = $ical->import_from_file(__DIR__ . '/resources/invalid.txt', 'UTF-8');
+ $this->assertEmpty($events);
+ }
+
+ /**
+ * Test some extended ical properties such as attendees, recurrence rules, alarms and attachments
+ *
+ * @depends test_import_from_file
+ */
+ function test_extended()
+ {
+ $ical = new libvcalendar();
+
+ $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
+ $event = $events[0];
+ $this->assertEquals('REQUEST', $ical->method, "iTip method");
+
+ // attendees
+ $this->assertEquals(2, count($event['attendees']), "Attendees list (including organizer)");
+ $organizer = $event['attendees'][0];
+ $this->assertEquals('ORGANIZER', $organizer['role'], 'Organizer ROLE');
+ $this->assertEquals('Rolf Test', $organizer['name'], 'Organizer name');
+
+ $attendee = $event['attendees'][1];
+ $this->assertEquals('REQ-PARTICIPANT', $attendee['role'], 'Attendee ROLE');
+ $this->assertEquals('NEEDS-ACTION', $attendee['status'], 'Attendee STATUS');
+ $this->assertEquals('rolf2 at mykolab.com', $attendee['email'], 'Attendee mailto:');
+ $this->assertTrue($attendee['rsvp'], 'Attendee RSVP');
+
+ // attachments
+ $this->assertEquals(1, count($event['attachments']), "Embedded attachments");
+ $attachment = $event['attachments'][0];
+ $this->assertEquals('text/html', $attachment['mimetype'], "Attachment mimetype attribute");
+ $this->assertEquals('calendar.html', $attachment['name'], "Attachment filename (X-LABEL) attribute");
+ $this->assertContains('<title>Kalender</title>', $attachment['data'], "Attachment content (decoded)");
+
+ // recurrence rules
+ $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+ $event = $events[0];
+
+ $this->assertTrue(is_array($event['recurrence']), 'Recurrences rule as hash array');
+ $rrule = $event['recurrence'];
+ $this->assertEquals('MONTHLY', $rrule['FREQ'], "Recurrence frequency");
+ $this->assertEquals('1', $rrule['INTERVAL'], "Recurrence interval");
+ $this->assertEquals('3WE', $rrule['BYDAY'], "Recurrence frequency");
+ $this->assertInstanceOf('DateTime', $rrule['UNTIL'], "Recurrence end date");
+
+ $this->assertEquals(2, count($rrule['EXDATE']), "Recurrence EXDATEs");
+ $this->assertInstanceOf('DateTime', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime");
+
+ // alarms
+ $this->assertEquals('-12H:DISPLAY', $event['alarms'], "Serialized alarms string");
+ $alarm = libcalendaring::parse_alaram_value($event['alarms']);
+ $this->assertEquals('12', $alarm[0], "Alarm value");
+ $this->assertEquals('-H', $alarm[1], "Alarm unit");
+
+ // categories, class
+ $this->assertEquals('libcalendaring tests', $event['categories'], "Event categories");
+ $this->assertEquals(2, $event['sensitivity'], "Class/sensitivity = confidential");
+ }
+
+ /**
+ * Test for iCal export from internal hash array representation
+ *
+ * @depend test_extended
+ */
+ function test_export()
+ {
+ $ical = new libvcalendar();
+
+ $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
+ $event = $events[0];
+ $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+ $event += $events[0];
+
+ $this->attachment_data = $event['attachments'][0]['data'];
+ unset($event['attachments'][0]['data']);
+ $event['attachments'][0]['id'] = '1';
+ $event['description'] = '*Exported by libvcalendar*';
+
+ $ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'));
+
+ $this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
+ $this->assertContains('METHOD:REQUEST', $ics, "iTip method");
+ $this->assertContains('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN");
+
+ $this->assertContains('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID");
+ $this->assertContains('SEQUENCE:' . $event['sequence'], $ics, "Export Sequence number");
+ $this->assertContains('CLASS:CONFIDENTIAL', $ics, "Sensitivity => Class");
+ $this->assertContains('DESCRIPTION:*Exported by', $ics, "Export Description");
+ $this->assertContains('ORGANIZER;CN="Rolf Test":mailto:rolf@', $ics, "Export organizer");
+ $this->assertRegExp('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/', $ics, "Export Attendee ROLE");
+ $this->assertRegExp('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/', $ics, "Export Attendee Status");
+ $this->assertRegExp('/ATTENDEE.*;RSVP=TRUE/', $ics, "Export Attendee RSVP");
+ $this->assertRegExp('/ATTENDEE.*:mailto:rolf2@/', $ics, "Export Attendee mailto:");
+
+ $rrule = $event['recurrence'];
+ $this->assertRegExp('/RRULE:.*FREQ='.$rrule['FREQ'].'/', $ics, "Export Recurrence Frequence");
+ $this->assertRegExp('/RRULE:.*INTERVAL='.$rrule['INTERVAL'].'/', $ics, "Export Recurrence Interval");
+ $this->assertRegExp('/RRULE:.*UNTIL=20140718/', $ics, "Export Recurrence End date");
+ $this->assertRegExp('/RRULE:.*BYDAY='.$rrule['BYDAY'].'/', $ics, "Export Recurrence BYDAY");
+ $this->assertRegExp('/EXDATE.*:20131218/', $ics, "Export Recurrence EXDATE");
+
+ $this->assertContains('BEGIN:VALARM', $ics, "Export VALARM");
+ $this->assertContains('TRIGGER:-PT12H', $ics, "Export Alarm trigger");
+
+ $this->assertRegExp('/ATTACH.*;VALUE=BINARY/', $ics, "Embed attachment");
+ $this->assertRegExp('/ATTACH.*;ENCODING=BASE64/', $ics, "Attachment B64 encoding");
+ $this->assertRegExp('!ATTACH.*;FMTTYPE=text/html!', $ics, "Attachment mimetype");
+ $this->assertRegExp('!ATTACH.*;X-LABEL=calendar.html!', $ics, "Attachment filename with X-LABEL");
+
+ $this->assertContains('END:VEVENT', $ics, "VEVENT encapsulation END");
+ $this->assertContains('END:VCALENDAR', $ics, "VCALENDAR encapsulation END");
+ }
+
+ /**
+ * @depend test_export
+ */
+ function test_export_multiple()
+ {
+ $ical = new libvcalendar();
+ $events = array_merge(
+ $ical->import_from_file(__DIR__ . '/resources/snd.ics', 'UTF-8'),
+ $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8')
+ );
+
+ $num = count($events);
+ $ics = $ical->export($events, null, false);
+
+ $this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
+ $this->assertContains('END:VCALENDAR', $ics, "VCALENDAR encapsulation END");
+ $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
+ $this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END");
+ }
+
+ /**
+ * @depend test_export
+ */
+ function test_export_recurrence_exceptions()
+ {
+ $ical = new libvcalendar();
+ $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+
+ // add exceptions
+ $event = $events[0];
+ $exception1 = $event;
+ $exception1['start'] = clone $event['start'];
+ $exception1['start']->setDate(2013, 8, 14);
+ $exception1['end'] = clone $event['end'];
+ $exception1['end']->setDate(2013, 8, 14);
+
+ $exception2 = $event;
+ $exception2['start'] = clone $event['start'];
+ $exception2['start']->setDate(2013, 11, 13);
+ $exception2['end'] = clone $event['end'];
+ $exception2['end']->setDate(2013, 11, 13);
+ $exception2['title'] = 'Recurring Exception';
+
+ $events[0]['recurrence']['EXCEPTIONS'] = array($exception1, $exception2);
+
+ $ics = $ical->export($events, null, false);
+
+ $num = count($events[0]['recurrence']['EXCEPTIONS']) + 1;
+ $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
+ $this->assertEquals($num, substr_count($ics, 'UID:'.$event['uid']), "Recurrence Exceptions with same UID");
+ $this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END");
+
+ $this->assertContains('RECURRENCE-ID:20130814', $ics, "Recurrence-ID (1) being the exception date");
+ $this->assertContains('RECURRENCE-ID:20131113', $ics, "Recurrence-ID (2) being the exception date");
+ $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title");
+ }
+
+ /**
+ * @depend test_export
+ */
+ function test_export_direct()
+ {
+ $ical = new libvcalendar();
+ $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
+ $num = count($events);
+
+ ob_start();
+ $return = $ical->export($events, null, true);
+ $output = ob_get_contents();
+ ob_end_clean();
+
+ $this->assertTrue($return, "Return true on successful writing");
+ $this->assertContains('BEGIN:VCALENDAR', $output, "VCALENDAR encapsulation BEGIN");
+ $this->assertContains('END:VCALENDAR', $output, "VCALENDAR encapsulation END");
+ $this->assertEquals($num, substr_count($output, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
+ $this->assertEquals($num, substr_count($output, 'END:VEVENT'), "VEVENT encapsulation END");
+ }
+
+ function get_attachment_data($id, $event)
+ {
+ return $this->attachment_data;
+ }
+}
+
diff --git a/plugins/libcalendaring/tests/resources/invalid.txt b/plugins/libcalendaring/tests/resources/invalid.txt
new file mode 100644
index 0000000..bb9c7bc
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/invalid.txt
@@ -0,0 +1,2 @@
+Some text file that has nothing to do with the iCal format.
+Just to test the sanity checks before attempting to parse iCal files.
\ No newline at end of file
diff --git a/plugins/libcalendaring/tests/resources/itip.ics b/plugins/libcalendaring/tests/resources/itip.ics
new file mode 100644
index 0000000..af283e3
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/itip.ics
@@ -0,0 +1,149 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN
+VERSION:2.0
+X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0
+METHOD:REQUEST
+BEGIN:VEVENT
+ORGANIZER;CN="Rolf Test":MAILTO:rolf at mykolab.com
+DTSTAMP:20130628T190056Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;
+ X-UID=208889384:mailto:rolf2 at mykolab.com
+CREATED:20130628T190032Z
+UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0
+LAST-MODIFIED:20130628T190032Z
+SUMMARY:iTip Test
+ATTACH;VALUE=BINARY;FMTTYPE=text/html;ENCODING=BASE64;
+ X-LABEL=calendar.html:
+ PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBYSFRNTCAxLjAgVHJhbnNpdGlvbm
+ FsLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSL3hodG1sMS9EVEQveGh0bWwxLXRyYW5zaXRp
+ b25hbC5kdGQiPgo8aHRtbD48aGVhZD4KICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LVR5cG
+ UiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD1VVEYtOCIgLz4KICA8dGl0bGU+S2FsZW5k
+ ZXI8L3RpdGxlPgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBib2R5IHsgYmFja2dyb3
+ VuZC1jb2xvcjp3aGl0ZTsgY29sb3I6YmxhY2sgfQogICAgdGQgeyB0ZXh0LWFsaWduOmNlbnRl
+ cjsgYmFja2dyb3VuZC1jb2xvcjojZWVlIH0KICAgIHRoIHsgdGV4dC1hbGlnbjpjZW50ZXI7IG
+ JhY2tncm91bmQtY29sb3I6IzIyODsgY29sb3I6d2hpdGUgfQogICAgdGQuc3VtIHsgdGV4dC1h
+ bGlnbjpsZWZ0IH0KICAgIHRkLnN1bWRvbmUgeyB0ZXh0LWFsaWduOmxlZnQ7IGJhY2tncm91bm
+ QtY29sb3I6I2NjYyB9CiAgICB0ZC5kb25lIHsgYmFja2dyb3VuZC1jb2xvcjojY2NjIH0KICAg
+ IHRkLnN1YmhlYWQgeyB0ZXh0LWFsaWduOmNlbnRlcjsgYmFja2dyb3VuZC1jb2xvcjojY2NmIH
+ 0KICAgIHRkLmRhdGVoZWFkIHsgdGV4dC1hbGlnbjpjZW50ZXI7IGJhY2tncm91bmQtY29sb3I6
+ I2NjZiB9CiAgICB0ZC5zcGFjZSB7IGJhY2tncm91bmQtY29sb3I6d2hpdGUgfQogICAgdGQuZG
+ F0ZSB7IHRleHQtYWxpZ246bGVmdCB9CiAgICB0ZC5kYXRlaG9saWRheSB7IHRleHQtYWxpZ246
+ bGVmdDsgY29sb3I6cmVkIH0KICA8L3N0eWxlPgo8L2hlYWQ+PGJvZHk+CjxoMT5LYWxlbmRlcj
+ wvaDE+CjxoMj5KdW5pIDIwMTM8L2gyPgo8dGFibGUgYm9yZGVyPSIxIj4KICA8dHI+PHRoPk1v
+ bnRhZzwvdGg+PHRoPkRpZW5zdGFnPC90aD48dGg+TWl0dHdvY2g8L3RoPjx0aD5Eb25uZXJzdG
+ FnPC90aD48dGg+RnJlaXRhZzwvdGg+PHRoPlNhbXN0YWc8L3RoPjx0aD5Tb25udGFnPC90aD48
+ L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPj
+ x0ZCBjbGFzcz0iZGF0ZSI+Mjc8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48
+ L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMC
+ I+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjg8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+
+ PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcm
+ Rlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjk8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249
+ InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYm
+ xlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MzA8L3RkPjwvdHI+PHRyPjx0ZCB2
+ YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcC
+ I+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MzE8L3RkPjwvdHI+PHRy
+ Pjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ2
+ 49InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTwvdGQ+PC90
+ cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIH
+ ZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saWRh
+ eSI+MjwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3
+ RkPgogIDwvdHI+CiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIw
+ Ij48dHI+PHRkIGNsYXNzPSJkYXRlIj4zPC90ZD48L3RyPjx0cj48dGQgdmFsaWduPSJ0b3AiPj
+ wvdGQ+PC90cj48L3RhYmxlPjwvdGQ+CiAgICA8dGQgdmFsaWduPSJ0b3AiPjx0YWJsZSBib3Jk
+ ZXI9IjAiPjx0cj48dGQgY2xhc3M9ImRhdGUiPjQ8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249In
+ RvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxl
+ IGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+NTwvdGQ+PC90cj48dHI+PHRkIHZhbG
+ lnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48
+ dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj42PC90ZD48L3RyPjx0cj48dG
+ QgdmFsaWduPSJ0b3AiPjwvdGQ+PC90cj48L3RhYmxlPjwvdGQ+CiAgICA8dGQgdmFsaWduPSJ0
+ b3AiPjx0YWJsZSBib3JkZXI9IjAiPjx0cj48dGQgY2xhc3M9ImRhdGUiPjc8L3RkPjwvdHI+PH
+ RyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxp
+ Z249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+ODwvdGQ+PC
+ 90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRk
+ IHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saW
+ RheSI+OTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48
+ L3RkPgogIDwvdHI+CiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPS
+ IwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMDwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9w
+ Ij48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm
+ 9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMTwvdGQ+PC90cj48dHI+PHRkIHZhbGln
+ bj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dG
+ FibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMjwvdGQ+PC90cj48dHI+PHRk
+ IHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG
+ 9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMzwvdGQ+PC90cj48
+ dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbG
+ lnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xNDwvdGQ+
+ PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPH
+ RkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4x
+ NTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPg
+ ogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJk
+ YXRlaG9saWRheSI+MTY8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPj
+ wvdGFibGU+PC90ZD4KICA8L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxl
+ IGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTc8L3RkPjwvdHI+PHRyPjx0ZCB2YW
+ xpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+
+ PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTg8L3RkPjwvdHI+PHRyPj
+ x0ZCB2YWxpZ249InRvcCI+PHRhYmxlPiAgPHRyPgogICAgPHRkPiZuYnNwOzwvdGQ+PHRkPiZu
+ YnNwOzwvdGQ+CiAgICA8dGQgY2xhc3M9InN1bSI+CiAgICAgIDxiPnRlcm1pbmJldHJlZmYxPC
+ 9iPgogICAgPC90ZD4KICA8dGQ+CiAgICAmbmJzcDsKICA8L3RkPgogIDx0ZD4KICAgICZuYnNw
+ OwogIDwvdGQ+CiAgPC90cj4KPC90YWJsZT48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPH
+ RkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4x
+ OTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT
+ 48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNs
+ YXNzPSJkYXRlIj4yMDwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPj
+ wvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIw
+ Ij48dHI+PHRkIGNsYXNzPSJkYXRlIj4yMTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj
+ 4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFi
+ bGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4yMjwvdGQ+PC90cj48dHI+PHRkIH
+ ZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGln
+ bj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saWRheSI+Mj
+ M8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwvdGFibGU+
+ PC90ZD4KICA8L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj
+ 0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MjQ8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRv
+ cCI+PHRhYmxlPiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj4xNTowMDwvdGQ+CiAgICA8dG
+ QgdmFsaWduPSJ0b3AiPjAwOjMwPC90ZD4KICAgIDx0ZCBjbGFzcz0ic3VtIj4KICAgICAgPGI+
+ dGVzdDwvYj4KICAgIDwvdGQ+CiAgPHRkPgogICAgJm5ic3A7CiAgPC90ZD4KICA8dGQ+CiAgIC
+ AmbmJzcDsKICA8L3RkPgogIDwvdHI+CjwvdGFibGU+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4K
+ ICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZG
+ F0ZSI+MjU8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwv
+ dGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPj
+ x0ZCBjbGFzcz0iZGF0ZSI+MjY8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7
+ PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcm
+ Rlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjc8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249
+ InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcC
+ I+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjg8L3RkPjwvdHI+PHRy
+ Pjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ2
+ 49InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjk8L3RkPjwv
+ dHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZC
+ B2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZWhvbGlk
+ YXkiPjMwPC90ZD48L3RyPjx0cj48dGQgdmFsaWduPSJ0b3AiPjwvdGQ+PC90cj48L3RhYmxlPj
+ wvdGQ+CiAgPC90cj4KPC90YWJsZT4KPGgxPkF1ZmdhYmVubGlzdGU8L2gxPgo8dGFibGUgYm9y
+ ZGVyPSIwIiBjZWxscGFkZGluZz0iMyIgY2VsbHNwYWNpbmc9IjMiPgogIDx0cj4KICAgIDx0aC
+ BjbGFzcz0ic3VtIj5BdWZnYWJlPC90aD4KICAgIDx0aD5Qcmlvcml0w6R0PC90aD4KICAgIDx0
+ aD5BYmdlc2NobG9zc2VuPC90aD4KICAgIDx0aD5Gw6RsbGlna2VpdHNkYXR1bTwvdGg+CiAgIC
+ A8dGg+T3J0PC90aD4KICAgIDx0aD5LYXRlZ29yaWVuPC90aD4KICA8L3RyPgo8dHI+CiAgPHRk
+ IGNsYXNzPSJzdW0iPgogICAgPGEgbmFtZT0iYzJmZjM1NGQtMjhlYi00YjBmLWIwYWItZDQxMT
+ g1Y2JlNGM3Ij48L2E+CiAgICA8Yj5UZXN0YXVmZ2FiZTE8L2I+CiAgICA8cD5BdWZnYWJlbmlu
+ aGFsdDE8L3A+CiAgICA8ZGl2IGFsaWduPSJyaWdodCI+PGEgaHJlZj0iI3N1YmMyZmYzNTRkLT
+ I4ZWItNGIwZi1iMGFiLWQ0MTE4NWNiZTRjNyI+VGVpbGF1ZmdhYmVuPC9hPjwvZGl2PgogIDwv
+ dGQ+CiAgPHRkPgogICAgMAogIDwvdGQ+CiAgPHRkPgogICAgMCAlCiAgPC90ZD4KICA8dGQ+Ci
+ AgICAyNS4wNi4yMDEzCiAgPC90ZD4KICA8dGQ+CiAgICAmbmJzcDsKICA8L3RkPgogIDx0ZD4K
+ ICAgICZuYnNwOwogIDwvdGQ+CjwvdHI+Cjx0cj4KICA8dGQgY2xhc3M9InN1bSI+CiAgICA8YS
+ BuYW1lPSI5MTc4NjRkNi1lN2ZiLTQwYTEtODEzOS05ODU3MzM0MDI3ZGYiPjwvYT4KICAgIDxi
+ PlRlc3RhdWZnYWJlNDg8L2I+CiAgPC90ZD4KICA8dGQ+CiAgICAwCiAgPC90ZD4KICA8dGQ+Ci
+ AgICAwICUKICA8L3RkPgogIDx0ZD4KICAgIDI2LjA2LjIwMTMKICA8L3RkPgogIDx0ZD4KICAg
+ ICZuYnNwOwogIDwvdGQ+CiAgPHRkPgogICAgdGVzdDQ4CiAgPC90ZD4KPC90cj4KICA8dHI+Ci
+ AgICA8dGQgY2xhc3M9InN1YmhlYWQiIGNvbHNwYW49IjYiPjxhIG5hbWU9InN1YmMyZmYzNTRk
+ LTI4ZWItNGIwZi1iMGFiLWQ0MTE4NWNiZTRjNyI+PC9hPlRlaWxhdWZnYWJlbiB2b246IDxhIG
+ hyZWY9IiNjMmZmMzU0ZC0yOGViLTRiMGYtYjBhYi1kNDExODVjYmU0YzciPjxiPlRlc3RhdWZn
+ YWJlMTwvYj48L2E+PC90ZD4KICA8L3RyPgo8dHI+CiAgPHRkIGNsYXNzPSJzdW0iPgogICAgPG
+ EgbmFtZT0iYWFlNzA2NDItNTRlMy00MTdhLTg1OGEtZmFiYjcwZTBhYmZiIj48L2E+CiAgICA8
+ Yj5VbnRlcmF1ZmdhYmU8L2I+CiAgPC90ZD4KICA8dGQ+CiAgICAwCiAgPC90ZD4KICA8dGQ+Ci
+ AgICAwICUKICA8L3RkPgogIDx0ZD4KICAgIDI1LjA2LjIwMTMKICA8L3RkPgogIDx0ZD4KICAg
+ ICZuYnNwOwogIDwvdGQ+CiAgPHRkPgogICAgJm5ic3A7CiAgPC90ZD4KPC90cj4KPC90YWJsZT
+ 4KPHA+RGllc2UgU2VpdGUgd3VyZGUgZXJzdGVsbHQgdm9uIFJvbGYgVGVzdCAobWFpbHRvOnJv
+ bGZAbXlrb2xhYi5jb20pIG1pdCBLT3JnYW5pemVyIChodHRwOi8va29yZ2FuaXplci5rZGUub3
+ JnKTwvcD4KPC9ib2R5PjwvaHRtbD4K
+DTSTART:20130703T183000Z
+DTEND:20130703T203000Z
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/plugins/libcalendaring/tests/resources/multiple.ics b/plugins/libcalendaring/tests/resources/multiple.ics
new file mode 100644
index 0000000..b0b0d17
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/multiple.ics
@@ -0,0 +1,51 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Zurich
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:CEST
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:CET
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20130530T140406Z
+UID:42524DA1-8B43-4CCA-9FDE-1A8F165115C6
+DTEND;TZID=Europe/Zurich:20130607T230000
+TRANSP:OPAQUE
+SUMMARY:Depeche Mode
+LAST-MODIFIED:20130530T140406Z
+DTSTAMP:20130530T140413Z
+DTSTART;TZID=Europe/Zurich:20130607T180000
+LOCATION:Wankdorf Stadium, Bern
+SEQUENCE:0
+BEGIN:VALARM
+UID:E5F5C5CB-F17A-4959-A0A2-80D700197425
+X-WR-ALARMUID:E5F5C5CB-F17A-4959-A0A2-80D700197425
+DESCRIPTION:Reminder
+TRIGGER:-PT1H
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20130718T073555Z
+UID:B968B885-08FB-40E5-B89E-6DA05F26AA79
+DTEND;VALUE=DATE:20130802
+TRANSP:TRANSPARENT
+SUMMARY:Swiss National Day
+DTSTART;VALUE=DATE:20130801
+DTSTAMP:20130718T074538Z
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
diff --git a/plugins/libcalendaring/tests/resources/recurring.ics b/plugins/libcalendaring/tests/resources/recurring.ics
new file mode 100644
index 0000000..92db9e2
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/recurring.ics
@@ -0,0 +1,43 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Zurich
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:CEST
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:CET
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+PRIORITY:3
+DTEND;TZID=Europe/Zurich:20130717T130000
+TRANSP:TRANSPARENT
+UID:7e93e8e8eef16f28aa33b78cd73613eb
+DTSTAMP:20130718T082032Z
+SEQUENCE:6
+CLASS:CONFIDENTIAL
+CATEGORIES:libcalendaring tests
+SUMMARY:Recurring Test
+LAST-MODIFIED:20120621
+DTSTART;TZID=Europe/Zurich:20130717T080000
+CREATED:20081223T232600Z
+RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=20140718T215959Z;BYDAY=3WE
+EXDATE;TZID=Europe/Zurich:20131218T080000
+EXDATE;TZID=Europe/Zurich:20140415T080000
+BEGIN:VALARM
+TRIGGER:-PT12H
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
diff --git a/plugins/libcalendaring/tests/resources/snd.ics b/plugins/libcalendaring/tests/resources/snd.ics
new file mode 100644
index 0000000..4efb2dd
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/snd.ics
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20130718T073555Z
+UID:B968B885-08FB-40E5-B89E-6DA05F26AA79
+URL;VALUE=URI:http://en.wikipedia.org/wiki/Swiss_National_Day
+DTEND;VALUE=DATE:20130802
+TRANSP:TRANSPARENT
+SUMMARY:Swiss National Day
+DTSTART;VALUE=DATE:20130801
+DTSTAMP:20130718T074538Z
+SEQUENCE:2
+DESCRIPTION:German: Schweizer Bundesfeier\nFrench: Fête nationale Suisse
+ \nItalian: Festa nazionale svizzera\nRomansh: Fiasta naziunala Svizra
+END:VEVENT
+END:VCALENDAR
commit 000bac244bc0b352410b55ba7e38f65ed2b6b3d9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 18 12:39:56 2013 +0200
Add symlink to moved Horde_Date class; move Horde import script, too
diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
new file mode 120000
index 0000000..5cd5ce2
--- /dev/null
+++ b/plugins/calendar/lib/Horde_Date.php
@@ -0,0 +1 @@
+../../libcalendaring/lib/Horde_Date.php
\ No newline at end of file
diff --git a/plugins/calendar/lib/get_horde_icalendar.sh b/plugins/calendar/lib/get_horde_icalendar.sh
deleted file mode 100755
index 1992bf2..0000000
--- a/plugins/calendar/lib/get_horde_icalendar.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/sh
-
-# Copy Horde_iCalendar classes and dependencies to stdout.
-# This will create a standalone copy of the classes requried for iCal parsing.
-
-SRCDIR=$1
-
-if [ ! -d "$SRCDIR" ]; then
- echo "Usage: get_horde_icalendar.sh SRCDIR"
- echo "Please enter a valid source directory of the Horde lib"
- exit 1
-fi
-
-echo "<?php
-
-/**
- * This is a concatenated copy of the following files:
- * Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
- * Pull the latest version of these file from the PEAR channel of the Horde project at http://pear.horde.org
- */
-
-require_once(dirname(__FILE__) . '/Horde_Date.php');"
-
-sed 's/<?php//; s/?>//' $SRCDIR/String.php
-echo "\n"
-sed 's/<?php//; s/?>//' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/"
-echo "\n"
-
-for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do
- sed 's/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
-done;
diff --git a/plugins/libcalendaring/get_horde_icalendar.sh b/plugins/libcalendaring/get_horde_icalendar.sh
new file mode 100755
index 0000000..1992bf2
--- /dev/null
+++ b/plugins/libcalendaring/get_horde_icalendar.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Copy Horde_iCalendar classes and dependencies to stdout.
+# This will create a standalone copy of the classes requried for iCal parsing.
+
+SRCDIR=$1
+
+if [ ! -d "$SRCDIR" ]; then
+ echo "Usage: get_horde_icalendar.sh SRCDIR"
+ echo "Please enter a valid source directory of the Horde lib"
+ exit 1
+fi
+
+echo "<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ * Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ * Pull the latest version of these file from the PEAR channel of the Horde project at http://pear.horde.org
+ */
+
+require_once(dirname(__FILE__) . '/Horde_Date.php');"
+
+sed 's/<?php//; s/?>//' $SRCDIR/String.php
+echo "\n"
+sed 's/<?php//; s/?>//' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/"
+echo "\n"
+
+for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do
+ sed 's/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
+done;
commit 18f9fa5c860f04622497fb822c4ca8d8022fe6f7
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 18 12:38:14 2013 +0200
Move iCal parsing/writing classes to libcalendaring
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 63bfe11..03cdb8b 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -219,8 +219,7 @@ class calendar extends rcube_plugin
public function get_ical()
{
if (!$this->ical) {
- require_once($this->home . '/lib/calendar_ical.php');
- $this->ical = new calendar_ical($this);
+ $this->ical = libcalendaring::get_ical();
}
return $this->ical;
diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
deleted file mode 100644
index 9197f84..0000000
--- a/plugins/calendar/lib/Horde_Date.php
+++ /dev/null
@@ -1,1304 +0,0 @@
-<?php
-
-/**
- * This is a concatenated copy of the following files:
- * Horde/Date/Utils.php, Horde/Date/Recurrence.php
- * Pull the latest version of these files from the PEAR channel of the Horde
- * project at http://pear.horde.org by installing the Horde_Date package.
- */
-
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * @category Horde
- * @package Date
- *
- * @TODO in format():
- * http://php.net/intldateformatter
- *
- * @TODO on timezones:
- * http://trac.agavi.org/ticket/1008
- * http://trac.agavi.org/changeset/3659
- *
- * @TODO on switching to PHP::DateTime:
- * The only thing ever stored in the database *IS* Unix timestamps. Doing
- * anything other than that is unmanageable, yet some frameworks use 'server
- * based' times in their systems, simply because they do not bother with
- * daylight saving and only 'serve' one timezone!
- *
- * The second you have to manage 'real' time across timezones then daylight
- * saving becomes essential, BUT only on the display side! Since the browser
- * only provides a time offset, this is useless and to be honest should simply
- * be ignored ( until it is upgraded to provide the correct information ;)
- * ). So we need a 'display' function that takes a simple numeric epoch, and a
- * separate timezone id into which the epoch is to be 'converted'. My W3C
- * mapping works simply because ADOdb then converts that to it's own simple
- * offset abbreviation - in my case GMT or BST. As long as DateTime passes the
- * full 64 bit number the date range from 100AD is also preserved ( and
- * further back if 2 digit years are disabled ). If I want to display the
- * 'real' timezone with this 'time' then I just add it in place of ADOdb's
- * 'timezone'. I am tempted to simply adjust the ADOdb class to take a
- * timezone in place of the simple GMT switch it currently uses.
- *
- * The return path is just the reverse and simply needs to take the client
- * display offset off prior to storage of the UTC epoch. SO we use
- * DateTimeZone to get an offset value for the clients timezone and simply add
- * or subtract this from a timezone agnostic display on the client end when
- * entering new times.
- *
- *
- * It's not really feasible to store dates in specific timezone, as most
- * national/local timezones support DST - and that is a pain to support, as
- * eg. sorting breaks when some timestamps get repeated. That's why it's
- * usually better to store datetimes as either UTC datetime or plain unix
- * timestamp. I usually go with the former - using database datetime type.
- */
-
-/**
- * @category Horde
- * @package Date
- */
-class Horde_Date
-{
- const DATE_SUNDAY = 0;
- const DATE_MONDAY = 1;
- const DATE_TUESDAY = 2;
- const DATE_WEDNESDAY = 3;
- const DATE_THURSDAY = 4;
- const DATE_FRIDAY = 5;
- const DATE_SATURDAY = 6;
-
- const MASK_SUNDAY = 1;
- const MASK_MONDAY = 2;
- const MASK_TUESDAY = 4;
- const MASK_WEDNESDAY = 8;
- const MASK_THURSDAY = 16;
- const MASK_FRIDAY = 32;
- const MASK_SATURDAY = 64;
- const MASK_WEEKDAYS = 62;
- const MASK_WEEKEND = 65;
- const MASK_ALLDAYS = 127;
-
- const MASK_SECOND = 1;
- const MASK_MINUTE = 2;
- const MASK_HOUR = 4;
- const MASK_DAY = 8;
- const MASK_MONTH = 16;
- const MASK_YEAR = 32;
- const MASK_ALLPARTS = 63;
-
- const DATE_DEFAULT = 'Y-m-d H:i:s';
- const DATE_JSON = 'Y-m-d\TH:i:s';
-
- /**
- * Year
- *
- * @var integer
- */
- protected $_year;
-
- /**
- * Month
- *
- * @var integer
- */
- protected $_month;
-
- /**
- * Day
- *
- * @var integer
- */
- protected $_mday;
-
- /**
- * Hour
- *
- * @var integer
- */
- protected $_hour = 0;
-
- /**
- * Minute
- *
- * @var integer
- */
- protected $_min = 0;
-
- /**
- * Second
- *
- * @var integer
- */
- protected $_sec = 0;
-
- /**
- * String representation of the date's timezone.
- *
- * @var string
- */
- protected $_timezone;
-
- /**
- * Default format for __toString()
- *
- * @var string
- */
- protected $_defaultFormat = self::DATE_DEFAULT;
-
- /**
- * Default specs that are always supported.
- * @var string
- */
- protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
-
- /**
- * Internally supported strftime() specifiers.
- * @var string
- */
- protected static $_supportedSpecs = '';
-
- /**
- * Map of required correction masks.
- *
- * @see __set()
- *
- * @var array
- */
- protected static $_corrections = array(
- 'year' => self::MASK_YEAR,
- 'month' => self::MASK_MONTH,
- 'mday' => self::MASK_DAY,
- 'hour' => self::MASK_HOUR,
- 'min' => self::MASK_MINUTE,
- 'sec' => self::MASK_SECOND,
- );
-
- protected $_formatCache = array();
-
- /**
- * Builds a new date object. If $date contains date parts, use them to
- * initialize the object.
- *
- * Recognized formats:
- * - arrays with keys 'year', 'month', 'mday', 'day'
- * 'hour', 'min', 'minute', 'sec'
- * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
- * - yyyy-mm-dd hh:mm:ss
- * - yyyymmddhhmmss
- * - yyyymmddThhmmssZ
- * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
- * 03 Mar 1973)
- * - unix timestamps
- * - anything parsed by strtotime()/DateTime.
- *
- * @throws Horde_Date_Exception
- */
- public function __construct($date = null, $timezone = null)
- {
- if (!self::$_supportedSpecs) {
- self::$_supportedSpecs = self::$_defaultSpecs;
- if (function_exists('nl_langinfo')) {
- self::$_supportedSpecs .= 'bBpxX';
- }
- }
-
- if (func_num_args() > 2) {
- // Handle args in order: year month day hour min sec tz
- $this->_initializeFromArgs(func_get_args());
- return;
- }
-
- $this->_initializeTimezone($timezone);
-
- if (is_null($date)) {
- return;
- }
-
- if (is_string($date)) {
- $date = trim($date, '"');
- }
-
- if (is_object($date)) {
- $this->_initializeFromObject($date);
- } elseif (is_array($date)) {
- $this->_initializeFromArray($date);
- } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $date, $parts)) {
- $this->_year = (int)$parts[1];
- $this->_month = (int)$parts[2];
- $this->_mday = (int)$parts[3];
- $this->_hour = (int)$parts[4];
- $this->_min = (int)$parts[5];
- $this->_sec = (int)$parts[6];
- if ($parts[7]) {
- $this->_initializeTimezone('UTC');
- }
- } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
- $parts[2] > 0 && $parts[2] <= 12 &&
- $parts[3] > 0 && $parts[3] <= 31) {
- $this->_year = (int)$parts[1];
- $this->_month = (int)$parts[2];
- $this->_mday = (int)$parts[3];
- $this->_hour = $this->_min = $this->_sec = 0;
- } elseif ((string)(int)$date == $date) {
- // Try as a timestamp.
- $parts = @getdate($date);
- if ($parts) {
- $this->_year = $parts['year'];
- $this->_month = $parts['mon'];
- $this->_mday = $parts['mday'];
- $this->_hour = $parts['hours'];
- $this->_min = $parts['minutes'];
- $this->_sec = $parts['seconds'];
- }
- } else {
- // Use date_create() so we can catch errors with PHP 5.2. Use
- // "new DateTime() once we require 5.3.
- $parsed = date_create($date);
- if (!$parsed) {
- throw new Horde_Date_Exception(sprintf(Horde_Date_Translation::t("Failed to parse time string (%s)"), $date));
- }
- $parsed->setTimezone(new DateTimeZone(date_default_timezone_get()));
- $this->_year = (int)$parsed->format('Y');
- $this->_month = (int)$parsed->format('m');
- $this->_mday = (int)$parsed->format('d');
- $this->_hour = (int)$parsed->format('H');
- $this->_min = (int)$parsed->format('i');
- $this->_sec = (int)$parsed->format('s');
- $this->_initializeTimezone(date_default_timezone_get());
- }
- }
-
- /**
- * Returns a simple string representation of the date object
- *
- * @return string This object converted to a string.
- */
- public function __toString()
- {
- try {
- return $this->format($this->_defaultFormat);
- } catch (Exception $e) {
- return '';
- }
- }
-
- /**
- * Returns a DateTime object representing this object.
- *
- * @return DateTime
- */
- public function toDateTime()
- {
- $date = new DateTime(null, new DateTimeZone($this->_timezone));
- $date->setDate($this->_year, $this->_month, $this->_mday);
- $date->setTime($this->_hour, $this->_min, $this->_sec);
- return $date;
- }
-
- /**
- * Converts a date in the proleptic Gregorian calendar to the no of days
- * since 24th November, 4714 B.C.
- *
- * Returns the no of days since Monday, 24th November, 4714 B.C. in the
- * proleptic Gregorian calendar (which is 24th November, -4713 using
- * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
- * proleptic Julian calendar). This is also the first day of the 'Julian
- * Period' proposed by Joseph Scaliger in 1583, and the number of days
- * since this date is known as the 'Julian Day'. (It is not directly
- * to do with the Julian calendar, although this is where the name
- * is derived from.)
- *
- * The algorithm is valid for all years (positive and negative), and
- * also for years preceding 4714 B.C.
- *
- * Algorithm is from PEAR::Date_Calc
- *
- * @author Monte Ohrt <monte at ispi.net>
- * @author Pierre-Alain Joye <pajoye at php.net>
- * @author Daniel Convissor <danielc at php.net>
- * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
- *
- * @return integer The number of days since 24th November, 4714 B.C.
- */
- public function toDays()
- {
- if (function_exists('GregorianToJD')) {
- return gregoriantojd($this->_month, $this->_mday, $this->_year);
- }
-
- $day = $this->_mday;
- $month = $this->_month;
- $year = $this->_year;
-
- if ($month > 2) {
- // March = 0, April = 1, ..., December = 9,
- // January = 10, February = 11
- $month -= 3;
- } else {
- $month += 9;
- --$year;
- }
-
- $hb_negativeyear = $year < 0;
- $century = intval($year / 100);
- $year = $year % 100;
-
- if ($hb_negativeyear) {
- // Subtract 1 because year 0 is a leap year;
- // And N.B. that we must treat the leap years as occurring
- // one year earlier than they do, because for the purposes
- // of calculation, the year starts on 1st March:
- //
- return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
- intval((1461 * $year + 1) / 4) +
- intval((153 * $month + 2) / 5) +
- $day + 1721118;
- } else {
- return intval(146097 * $century / 4) +
- intval(1461 * $year / 4) +
- intval((153 * $month + 2) / 5) +
- $day + 1721119;
- }
- }
-
- /**
- * Converts number of days since 24th November, 4714 B.C. (in the proleptic
- * Gregorian calendar, which is year -4713 using 'Astronomical' year
- * numbering) to Gregorian calendar date.
- *
- * Returned date belongs to the proleptic Gregorian calendar, using
- * 'Astronomical' year numbering.
- *
- * The algorithm is valid for all years (positive and negative), and
- * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
- * and so the only limitation is platform-dependent (for 32-bit systems
- * the maximum year would be something like about 1,465,190 A.D.).
- *
- * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
- *
- * Algorithm is from PEAR::Date_Calc
- *
- * @author Monte Ohrt <monte at ispi.net>
- * @author Pierre-Alain Joye <pajoye at php.net>
- * @author Daniel Convissor <danielc at php.net>
- * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
- *
- * @param int $days the number of days since 24th November, 4714 B.C.
- * @param string $format the string indicating how to format the output
- *
- * @return Horde_Date A Horde_Date object representing the date.
- */
- public static function fromDays($days)
- {
- if (function_exists('JDToGregorian')) {
- list($month, $day, $year) = explode('/', JDToGregorian($days));
- } else {
- $days = intval($days);
-
- $days -= 1721119;
- $century = floor((4 * $days - 1) / 146097);
- $days = floor(4 * $days - 1 - 146097 * $century);
- $day = floor($days / 4);
-
- $year = floor((4 * $day + 3) / 1461);
- $day = floor(4 * $day + 3 - 1461 * $year);
- $day = floor(($day + 4) / 4);
-
- $month = floor((5 * $day - 3) / 153);
- $day = floor(5 * $day - 3 - 153 * $month);
- $day = floor(($day + 5) / 5);
-
- $year = $century * 100 + $year;
- if ($month < 10) {
- $month +=3;
- } else {
- $month -=9;
- ++$year;
- }
- }
-
- return new Horde_Date($year, $month, $day);
- }
-
- /**
- * Getter for the date and time properties.
- *
- * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
- * 'sec'.
- *
- * @return integer The property value, or null if not set.
- */
- public function __get($name)
- {
- if ($name == 'day') {
- $name = 'mday';
- }
-
- return $this->{'_' . $name};
- }
-
- /**
- * Setter for the date and time properties.
- *
- * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
- * 'sec'.
- * @param integer $value The property value.
- */
- public function __set($name, $value)
- {
- if ($name == 'timezone') {
- $this->_initializeTimezone($value);
- return;
- }
- if ($name == 'day') {
- $name = 'mday';
- }
-
- if ($name != 'year' && $name != 'month' && $name != 'mday' &&
- $name != 'hour' && $name != 'min' && $name != 'sec') {
- throw new InvalidArgumentException('Undefined property ' . $name);
- }
-
- $down = $value < $this->{'_' . $name};
- $this->{'_' . $name} = $value;
- $this->_correct(self::$_corrections[$name], $down);
- $this->_formatCache = array();
- }
-
- /**
- * Returns whether a date or time property exists.
- *
- * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
- * 'sec'.
- *
- * @return boolen True if the property exists and is set.
- */
- public function __isset($name)
- {
- if ($name == 'day') {
- $name = 'mday';
- }
- return ($name == 'year' || $name == 'month' || $name == 'mday' ||
- $name == 'hour' || $name == 'min' || $name == 'sec') &&
- isset($this->{'_' . $name});
- }
-
- /**
- * Adds a number of seconds or units to this date, returning a new Date
- * object.
- */
- public function add($factor)
- {
- $d = clone($this);
- if (is_array($factor) || is_object($factor)) {
- foreach ($factor as $property => $value) {
- $d->$property += $value;
- }
- } else {
- $d->sec += $factor;
- }
-
- return $d;
- }
-
- /**
- * Subtracts a number of seconds or units from this date, returning a new
- * Horde_Date object.
- */
- public function sub($factor)
- {
- if (is_array($factor)) {
- foreach ($factor as &$value) {
- $value *= -1;
- }
- } else {
- $factor *= -1;
- }
-
- return $this->add($factor);
- }
-
- /**
- * Converts this object to a different timezone.
- *
- * @param string $timezone The new timezone.
- *
- * @return Horde_Date This object.
- */
- public function setTimezone($timezone)
- {
- $date = $this->toDateTime();
- $date->setTimezone(new DateTimeZone($timezone));
- $this->_timezone = $timezone;
- $this->_year = (int)$date->format('Y');
- $this->_month = (int)$date->format('m');
- $this->_mday = (int)$date->format('d');
- $this->_hour = (int)$date->format('H');
- $this->_min = (int)$date->format('i');
- $this->_sec = (int)$date->format('s');
- $this->_formatCache = array();
- return $this;
- }
-
- /**
- * Sets the default date format used in __toString()
- *
- * @param string $format
- */
- public function setDefaultFormat($format)
- {
- $this->_defaultFormat = $format;
- }
-
- /**
- * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
- *
- * @return integer The day of the week.
- */
- public function dayOfWeek()
- {
- if ($this->_month > 2) {
- $month = $this->_month - 2;
- $year = $this->_year;
- } else {
- $month = $this->_month + 10;
- $year = $this->_year - 1;
- }
-
- $day = (floor((13 * $month - 1) / 5) +
- $this->_mday + ($year % 100) +
- floor(($year % 100) / 4) +
- floor(($year / 100) / 4) - 2 *
- floor($year / 100) + 77);
-
- return (int)($day - 7 * floor($day / 7));
- }
-
- /**
- * Returns the day number of the year (1 to 365/366).
- *
- * @return integer The day of the year.
- */
- public function dayOfYear()
- {
- return $this->format('z') + 1;
- }
-
- /**
- * Returns the week of the month.
- *
- * @return integer The week number.
- */
- public function weekOfMonth()
- {
- return ceil($this->_mday / 7);
- }
-
- /**
- * Returns the week of the year, first Monday is first day of first week.
- *
- * @return integer The week number.
- */
- public function weekOfYear()
- {
- return $this->format('W');
- }
-
- /**
- * Returns the number of weeks in the given year (52 or 53).
- *
- * @param integer $year The year to count the number of weeks in.
- *
- * @return integer $numWeeks The number of weeks in $year.
- */
- public static function weeksInYear($year)
- {
- // Find the last Thursday of the year.
- $date = new Horde_Date($year . '-12-31');
- while ($date->dayOfWeek() != self::DATE_THURSDAY) {
- --$date->mday;
- }
- return $date->weekOfYear();
- }
-
- /**
- * Sets the date of this object to the $nth weekday of $weekday.
- *
- * @param integer $weekday The day of the week (0 = Sunday, etc).
- * @param integer $nth The $nth $weekday to set to (defaults to 1).
- */
- public function setNthWeekday($weekday, $nth = 1)
- {
- if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
- return;
- }
-
- if ($nth < 0) { // last $weekday of month
- $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
- $last = $this->dayOfWeek();
- $this->_mday += ($weekday - $last);
- if ($this->_mday > $lastday)
- $this->_mday -= 7;
- }
- else {
- $this->_mday = 1;
- $first = $this->dayOfWeek();
- if ($weekday < $first) {
- $this->_mday = 8 + $weekday - $first;
- } else {
- $this->_mday = $weekday - $first + 1;
- }
- $diff = 7 * $nth - 7;
- $this->_mday += $diff;
- $this->_correct(self::MASK_DAY, $diff < 0);
- }
- }
-
- /**
- * Is the date currently represented by this object a valid date?
- *
- * @return boolean Validity, counting leap years, etc.
- */
- public function isValid()
- {
- return ($this->_year >= 0 && $this->_year <= 9999);
- }
-
- /**
- * Compares this date to another date object to see which one is
- * greater (later). Assumes that the dates are in the same
- * timezone.
- *
- * @param mixed $other The date to compare to.
- *
- * @return integer == 0 if they are on the same date
- * >= 1 if $this is greater (later)
- * <= -1 if $other is greater (later)
- */
- public function compareDate($other)
- {
- if (!($other instanceof Horde_Date)) {
- $other = new Horde_Date($other);
- }
-
- if ($this->_year != $other->year) {
- return $this->_year - $other->year;
- }
- if ($this->_month != $other->month) {
- return $this->_month - $other->month;
- }
-
- return $this->_mday - $other->mday;
- }
-
- /**
- * Returns whether this date is after the other.
- *
- * @param mixed $other The date to compare to.
- *
- * @return boolean True if this date is after the other.
- */
- public function after($other)
- {
- return $this->compareDate($other) > 0;
- }
-
- /**
- * Returns whether this date is before the other.
- *
- * @param mixed $other The date to compare to.
- *
- * @return boolean True if this date is before the other.
- */
- public function before($other)
- {
- return $this->compareDate($other) < 0;
- }
-
- /**
- * Returns whether this date is the same like the other.
- *
- * @param mixed $other The date to compare to.
- *
- * @return boolean True if this date is the same like the other.
- */
- public function equals($other)
- {
- return $this->compareDate($other) == 0;
- }
-
- /**
- * Compares this to another date object by time, to see which one
- * is greater (later). Assumes that the dates are in the same
- * timezone.
- *
- * @param mixed $other The date to compare to.
- *
- * @return integer == 0 if they are at the same time
- * >= 1 if $this is greater (later)
- * <= -1 if $other is greater (later)
- */
- public function compareTime($other)
- {
- if (!($other instanceof Horde_Date)) {
- $other = new Horde_Date($other);
- }
-
- if ($this->_hour != $other->hour) {
- return $this->_hour - $other->hour;
- }
- if ($this->_min != $other->min) {
- return $this->_min - $other->min;
- }
-
- return $this->_sec - $other->sec;
- }
-
- /**
- * Compares this to another date object, including times, to see
- * which one is greater (later). Assumes that the dates are in the
- * same timezone.
- *
- * @param mixed $other The date to compare to.
- *
- * @return integer == 0 if they are equal
- * >= 1 if $this is greater (later)
- * <= -1 if $other is greater (later)
- */
- public function compareDateTime($other)
- {
- if (!($other instanceof Horde_Date)) {
- $other = new Horde_Date($other);
- }
-
- if ($diff = $this->compareDate($other)) {
- return $diff;
- }
-
- return $this->compareTime($other);
- }
-
- /**
- * Returns number of days between this date and another.
- *
- * @param Horde_Date $other The other day to diff with.
- *
- * @return integer The absolute number of days between the two dates.
- */
- public function diff($other)
- {
- return abs($this->toDays() - $other->toDays());
- }
-
- /**
- * Returns the time offset for local time zone.
- *
- * @param boolean $colon Place a colon between hours and minutes?
- *
- * @return string Timezone offset as a string in the format +HH:MM.
- */
- public function tzOffset($colon = true)
- {
- return $colon ? $this->format('P') : $this->format('O');
- }
-
- /**
- * Returns the unix timestamp representation of this date.
- *
- * @return integer A unix timestamp.
- */
- public function timestamp()
- {
- if ($this->_year >= 1970 && $this->_year < 2038) {
- return mktime($this->_hour, $this->_min, $this->_sec,
- $this->_month, $this->_mday, $this->_year);
- }
- return $this->format('U');
- }
-
- /**
- * Returns the unix timestamp representation of this date, 12:00am.
- *
- * @return integer A unix timestamp.
- */
- public function datestamp()
- {
- if ($this->_year >= 1970 && $this->_year < 2038) {
- return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
- }
- $date = new DateTime($this->format('Y-m-d'));
- return $date->format('U');
- }
-
- /**
- * Formats date and time to be passed around as a short url parameter.
- *
- * @return string Date and time.
- */
- public function dateString()
- {
- return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
- }
-
- /**
- * Formats date and time to the ISO format used by JSON.
- *
- * @return string Date and time.
- */
- public function toJson()
- {
- return $this->format(self::DATE_JSON);
- }
-
- /**
- * Formats date and time to the RFC 2445 iCalendar DATE-TIME format.
- *
- * @param boolean $floating Whether to return a floating date-time
- * (without time zone information).
- *
- * @return string Date and time.
- */
- public function toiCalendar($floating = false)
- {
- if ($floating) {
- return $this->format('Ymd\THis');
- }
- $dateTime = $this->toDateTime();
- $dateTime->setTimezone(new DateTimeZone('UTC'));
- return $dateTime->format('Ymd\THis\Z');
- }
-
- /**
- * Formats time using the specifiers available in date() or in the DateTime
- * class' format() method.
- *
- * To format in languages other than English, use strftime() instead.
- *
- * @param string $format
- *
- * @return string Formatted time.
- */
- public function format($format)
- {
- if (!isset($this->_formatCache[$format])) {
- $this->_formatCache[$format] = $this->toDateTime()->format($format);
- }
- return $this->_formatCache[$format];
- }
-
- /**
- * Formats date and time using strftime() format.
- *
- * @return string strftime() formatted date and time.
- */
- public function strftime($format)
- {
- if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
- return strftime($format, $this->timestamp());
- } else {
- return $this->_strftime($format);
- }
- }
-
- /**
- * Formats date and time using a limited set of the strftime() format.
- *
- * @return string strftime() formatted date and time.
- */
- protected function _strftime($format)
- {
- return preg_replace(
- array('/%b/e',
- '/%B/e',
- '/%C/e',
- '/%d/e',
- '/%D/e',
- '/%e/e',
- '/%H/e',
- '/%I/e',
- '/%m/e',
- '/%M/e',
- '/%n/',
- '/%p/e',
- '/%R/e',
- '/%S/e',
- '/%t/',
- '/%T/e',
- '/%x/e',
- '/%X/e',
- '/%y/e',
- '/%Y/',
- '/%%/'),
- array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
- '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
- '(int)($this->_year / 100)',
- 'sprintf(\'%02d\', $this->_mday)',
- '$this->_strftime(\'%m/%d/%y\')',
- 'sprintf(\'%2d\', $this->_mday)',
- 'sprintf(\'%02d\', $this->_hour)',
- 'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
- 'sprintf(\'%02d\', $this->_month)',
- 'sprintf(\'%02d\', $this->_min)',
- "\n",
- '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
- '$this->_strftime(\'%H:%M\')',
- 'sprintf(\'%02d\', $this->_sec)',
- "\t",
- '$this->_strftime(\'%H:%M:%S\')',
- '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))',
- '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))',
- 'substr(sprintf(\'%04d\', $this->_year), -2)',
- (int)$this->_year,
- '%'),
- $format);
- }
-
- /**
- * Corrects any over- or underflows in any of the date's members.
- *
- * @param integer $mask We may not want to correct some overflows.
- * @param integer $down Whether to correct the date up or down.
- */
- protected function _correct($mask = self::MASK_ALLPARTS, $down = false)
- {
- if ($mask & self::MASK_SECOND) {
- if ($this->_sec < 0 || $this->_sec > 59) {
- $mask |= self::MASK_MINUTE;
-
- $this->_min += (int)($this->_sec / 60);
- $this->_sec %= 60;
- if ($this->_sec < 0) {
- $this->_min--;
- $this->_sec += 60;
- }
- }
- }
-
- if ($mask & self::MASK_MINUTE) {
- if ($this->_min < 0 || $this->_min > 59) {
- $mask |= self::MASK_HOUR;
-
- $this->_hour += (int)($this->_min / 60);
- $this->_min %= 60;
- if ($this->_min < 0) {
- $this->_hour--;
- $this->_min += 60;
- }
- }
- }
-
- if ($mask & self::MASK_HOUR) {
- if ($this->_hour < 0 || $this->_hour > 23) {
- $mask |= self::MASK_DAY;
-
- $this->_mday += (int)($this->_hour / 24);
- $this->_hour %= 24;
- if ($this->_hour < 0) {
- $this->_mday--;
- $this->_hour += 24;
- }
- }
- }
-
- if ($mask & self::MASK_MONTH) {
- $this->_correctMonth($down);
- /* When correcting the month, always correct the day too. Months
- * have different numbers of days. */
- $mask |= self::MASK_DAY;
- }
-
- if ($mask & self::MASK_DAY) {
- while ($this->_mday > 28 &&
- $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) {
- if ($down) {
- $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
- } else {
- $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
- $this->_month++;
- }
- $this->_correctMonth($down);
- }
- while ($this->_mday < 1) {
- --$this->_month;
- $this->_correctMonth($down);
- $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
- }
- }
- }
-
- /**
- * Corrects the current month.
- *
- * This cannot be done in _correct() because that would also trigger a
- * correction of the day, which would result in an infinite loop.
- *
- * @param integer $down Whether to correct the date up or down.
- */
- protected function _correctMonth($down = false)
- {
- $this->_year += (int)($this->_month / 12);
- $this->_month %= 12;
- if ($this->_month < 1) {
- $this->_year--;
- $this->_month += 12;
- }
- }
-
- /**
- * Handles args in order: year month day hour min sec tz
- */
- protected function _initializeFromArgs($args)
- {
- $tz = (isset($args[6])) ? array_pop($args) : null;
- $this->_initializeTimezone($tz);
-
- $args = array_slice($args, 0, 6);
- $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
- $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
- $date = array_merge($keys, $date);
-
- $this->_initializeFromArray($date);
- }
-
- protected function _initializeFromArray($date)
- {
- if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
- if ($date['year'] > 70) {
- $date['year'] += 1900;
- } else {
- $date['year'] += 2000;
- }
- }
-
- foreach ($date as $key => $val) {
- if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
- $this->{'_'. $key} = (int)$val;
- }
- }
-
- // If $date['day'] is present and numeric we may have been passed
- // a Horde_Form_datetime array.
- if (isset($date['day']) &&
- (string)(int)$date['day'] == $date['day']) {
- $this->_mday = (int)$date['day'];
- }
- // 'minute' key also from Horde_Form_datetime
- if (isset($date['minute']) &&
- (string)(int)$date['minute'] == $date['minute']) {
- $this->_min = (int)$date['minute'];
- }
-
- $this->_correct();
- }
-
- protected function _initializeFromObject($date)
- {
- if ($date instanceof DateTime) {
- $this->_year = (int)$date->format('Y');
- $this->_month = (int)$date->format('m');
- $this->_mday = (int)$date->format('d');
- $this->_hour = (int)$date->format('H');
- $this->_min = (int)$date->format('i');
- $this->_sec = (int)$date->format('s');
- $this->_initializeTimezone($date->getTimezone()->getName());
- } else {
- $is_horde_date = $date instanceof Horde_Date;
- foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
- if ($is_horde_date || isset($date->$key)) {
- $this->{'_' . $key} = (int)$date->$key;
- }
- }
- if (!$is_horde_date) {
- $this->_correct();
- } else {
- $this->_initializeTimezone($date->timezone);
- }
- }
- }
-
- protected function _initializeTimezone($timezone)
- {
- if (empty($timezone)) {
- $timezone = date_default_timezone_get();
- }
- $this->_timezone = $timezone;
- }
-
-}
-
-/**
- * @category Horde
- * @package Date
- */
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * @category Horde
- * @package Date
- */
-class Horde_Date_Utils
-{
- /**
- * Returns whether a year is a leap year.
- *
- * @param integer $year The year.
- *
- * @return boolean True if the year is a leap year.
- */
- public static function isLeapYear($year)
- {
- if (strlen($year) != 4 || preg_match('/\D/', $year)) {
- return false;
- }
-
- return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
- }
-
- /**
- * Returns the date of the year that corresponds to the first day of the
- * given week.
- *
- * @param integer $week The week of the year to find the first day of.
- * @param integer $year The year to calculate for.
- *
- * @return Horde_Date The date of the first day of the given week.
- */
- public static function firstDayOfWeek($week, $year)
- {
- return new Horde_Date(sprintf('%04dW%02d', $year, $week));
- }
-
- /**
- * Returns the number of days in the specified month.
- *
- * @param integer $month The month
- * @param integer $year The year.
- *
- * @return integer The number of days in the month.
- */
- public static function daysInMonth($month, $year)
- {
- static $cache = array();
- if (!isset($cache[$year][$month])) {
- $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
- $cache[$year][$month] = $date->format('t');
- }
- return $cache[$year][$month];
- }
-
- /**
- * Returns a relative, natural language representation of a timestamp
- *
- * @todo Wider range of values ... maybe future time as well?
- * @todo Support minimum resolution parameter.
- *
- * @param mixed $time The time. Any format accepted by Horde_Date.
- * @param string $date_format Format to display date if timestamp is
- * more then 1 day old.
- * @param string $time_format Format to display time if timestamp is 1
- * day old.
- *
- * @return string The relative time (i.e. 2 minutes ago)
- */
- public static function relativeDateTime($time, $date_format = '%x',
- $time_format = '%X')
- {
- $date = new Horde_Date($time);
-
- $delta = time() - $date->timestamp();
- if ($delta < 60) {
- return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta);
- }
-
- $delta = round($delta / 60);
- if ($delta < 60) {
- return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta);
- }
-
- $delta = round($delta / 60);
- if ($delta < 24) {
- return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta);
- }
-
- if ($delta > 24 && $delta < 48) {
- $date = new Horde_Date($time);
- return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format));
- }
-
- $delta = round($delta / 24);
- if ($delta < 7) {
- return sprintf(Horde_Date_Translation::t("%d days ago"), $delta);
- }
-
- if (round($delta / 7) < 5) {
- $delta = round($delta / 7);
- return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta);
- }
-
- // Default to the user specified date format.
- return $date->strftime($date_format);
- }
-
- /**
- * Tries to convert strftime() formatters to date() formatters.
- *
- * Unsupported formatters will be removed.
- *
- * @param string $format A strftime() formatting string.
- *
- * @return string A date() formatting string.
- */
- public static function strftime2date($format)
- {
- $replace = array(
- '/%a/' => 'D',
- '/%A/' => 'l',
- '/%d/' => 'd',
- '/%e/' => 'j',
- '/%j/' => 'z',
- '/%u/' => 'N',
- '/%w/' => 'w',
- '/%U/' => '',
- '/%V/' => 'W',
- '/%W/' => '',
- '/%b/' => 'M',
- '/%B/' => 'F',
- '/%h/' => 'M',
- '/%m/' => 'm',
- '/%C/' => '',
- '/%g/' => '',
- '/%G/' => 'o',
- '/%y/' => 'y',
- '/%Y/' => 'Y',
- '/%H/' => 'H',
- '/%I/' => 'h',
- '/%i/' => 'g',
- '/%M/' => 'i',
- '/%p/' => 'A',
- '/%P/' => 'a',
- '/%r/' => 'h:i:s A',
- '/%R/' => 'H:i',
- '/%S/' => 's',
- '/%T/' => 'H:i:s',
- '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))',
- '/%z/' => 'O',
- '/%Z/' => '',
- '/%c/' => '',
- '/%D/' => 'm/d/y',
- '/%F/' => 'Y-m-d',
- '/%s/' => 'U',
- '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))',
- '/%n/' => "\n",
- '/%t/' => "\t",
- '/%%/' => '%'
- );
-
- return preg_replace(array_keys($replace), array_values($replace), $format);
- }
-
-}
diff --git a/plugins/calendar/lib/Horde_iCalendar.php b/plugins/calendar/lib/Horde_iCalendar.php
deleted file mode 100644
index 6d75d27..0000000
--- a/plugins/calendar/lib/Horde_iCalendar.php
+++ /dev/null
@@ -1,3300 +0,0 @@
-<?php
-
-/**
- * This is a concatenated copy of the following files:
- * Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
- */
-
-if (!class_exists('Horde_Date'))
- require_once(dirname(__FILE__) . '/Horde_Date.php');
-
-
-$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
-
-/**
- * The String:: class provides static methods for charset and locale safe
- * string manipulation.
- *
- * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Jan Schneider <jan at horde.org>
- * @since Horde 3.0
- * @package Horde_Util
- */
-class String {
-
- /**
- * Caches the result of extension_loaded() calls.
- *
- * @param string $ext The extension name.
- *
- * @return boolean Is the extension loaded?
- *
- * @see Util::extensionExists()
- */
- function extensionExists($ext)
- {
- static $cache = array();
-
- if (!isset($cache[$ext])) {
- $cache[$ext] = extension_loaded($ext);
- }
-
- return $cache[$ext];
- }
-
- /**
- * Sets a default charset that the String:: methods will use if none is
- * explicitly specified.
- *
- * @param string $charset The charset to use as the default one.
- */
- function setDefaultCharset($charset)
- {
- $GLOBALS['_HORDE_STRING_CHARSET'] = $charset;
- if (String::extensionExists('mbstring') &&
- function_exists('mb_regex_encoding')) {
- $old_error = error_reporting(0);
- mb_regex_encoding(String::_mbstringCharset($charset));
- error_reporting($old_error);
- }
- }
-
- /**
- * Converts a string from one charset to another.
- *
- * Works only if either the iconv or the mbstring extension
- * are present and best if both are available.
- * The original string is returned if conversion failed or none
- * of the extensions were available.
- *
- * @param mixed $input The data to be converted. If $input is an an array,
- * the array's values get converted recursively.
- * @param string $from The string's current charset.
- * @param string $to The charset to convert the string to. If not
- * specified, the global variable
- * $_HORDE_STRING_CHARSET will be used.
- *
- * @return mixed The converted input data.
- */
- function convertCharset($input, $from, $to = null)
- {
- /* Don't bother converting numbers. */
- if (is_numeric($input)) {
- return $input;
- }
-
- /* Get the user's default character set if none passed in. */
- if (is_null($to)) {
- $to = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
-
- /* If the from and to character sets are identical, return now. */
- if ($from == $to) {
- return $input;
- }
- $from = String::lower($from);
- $to = String::lower($to);
- if ($from == $to) {
- return $input;
- }
-
- if (is_array($input)) {
- $tmp = array();
- reset($input);
- while (list($key, $val) = each($input)) {
- $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to);
- }
- return $tmp;
- }
- if (is_object($input)) {
- // PEAR_Error objects are almost guaranteed to contain recursion,
- // which will cause a segfault in PHP. We should never reach
- // this line, but add a check and a log message to help the devs
- // track down and fix this issue.
- if (is_a($input, 'PEAR_Error')) {
- Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
- return '';
- }
- $vars = get_object_vars($input);
- while (list($key, $val) = each($vars)) {
- $input->$key = String::convertCharset($val, $from, $to);
- }
- return $input;
- }
-
- if (!is_string($input)) {
- return $input;
- }
-
- return String::_convertCharset($input, $from, $to);
- }
-
- /**
- * Internal function used to do charset conversion.
- *
- * @access private
- *
- * @param string $input See String::convertCharset().
- * @param string $from See String::convertCharset().
- * @param string $to See String::convertCharset().
- *
- * @return string The converted string.
- */
- function _convertCharset($input, $from, $to)
- {
- $output = '';
- $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii'));
- $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii'));
-
- /* Use utf8_[en|de]code() if possible and if the string isn't too
- * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these
- * functions use more memory. */
- if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) {
- if ($from_check && ($to == 'utf-8')) {
- return utf8_encode($input);
- }
-
- if (($from == 'utf-8') && $to_check) {
- return utf8_decode($input);
- }
- }
-
- /* First try iconv with transliteration. */
- if (($from != 'utf7-imap') &&
- ($to != 'utf7-imap') &&
- String::extensionExists('iconv')) {
- /* We need to tack an extra character temporarily because of a bug
- * in iconv() if the last character is not a 7 bit ASCII
- * character. */
- $oldTrackErrors = ini_set('track_errors', 1);
- unset($php_errormsg);
- $output = @iconv($from, $to . '//TRANSLIT', $input . 'x');
- $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to);
- ini_set('track_errors', $oldTrackErrors);
- }
-
- /* Next try mbstring. */
- if (!$output && String::extensionExists('mbstring')) {
- $old_error = error_reporting(0);
- $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from));
- error_reporting($old_error);
- }
-
- /* At last try imap_utf7_[en|de]code if appropriate. */
- if (!$output && String::extensionExists('imap')) {
- if ($from_check && ($to == 'utf7-imap')) {
- return @imap_utf7_encode($input);
- }
- if (($from == 'utf7-imap') && $to_check) {
- return @imap_utf7_decode($input);
- }
- }
-
- return (!$output) ? $input : $output;
- }
-
- /**
- * Makes a string lowercase.
- *
- * @param string $string The string to be converted.
- * @param boolean $locale If true the string will be converted based on a
- * given charset, locale independent else.
- * @param string $charset If $locale is true, the charset to use when
- * converting. If not provided the current charset.
- *
- * @return string The string with lowercase characters
- */
- function lower($string, $locale = false, $charset = null)
- {
- static $lowers;
-
- if ($locale) {
- /* The existence of mb_strtolower() depends on the platform. */
- if (String::extensionExists('mbstring') &&
- function_exists('mb_strtolower')) {
- if (is_null($charset)) {
- $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
- $old_error = error_reporting(0);
- $ret = mb_strtolower($string, String::_mbstringCharset($charset));
- error_reporting($old_error);
- if (!empty($ret)) {
- return $ret;
- }
- }
- return strtolower($string);
- }
-
- if (!isset($lowers)) {
- $lowers = array();
- }
- if (!isset($lowers[$string])) {
- $language = setlocale(LC_CTYPE, 0);
- setlocale(LC_CTYPE, 'C');
- $lowers[$string] = strtolower($string);
- setlocale(LC_CTYPE, $language);
- }
-
- return $lowers[$string];
- }
-
- /**
- * Makes a string uppercase.
- *
- * @param string $string The string to be converted.
- * @param boolean $locale If true the string will be converted based on a
- * given charset, locale independent else.
- * @param string $charset If $locale is true, the charset to use when
- * converting. If not provided the current charset.
- *
- * @return string The string with uppercase characters
- */
- function upper($string, $locale = false, $charset = null)
- {
- static $uppers;
-
- if ($locale) {
- /* The existence of mb_strtoupper() depends on the
- * platform. */
- if (function_exists('mb_strtoupper')) {
- if (is_null($charset)) {
- $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
- $old_error = error_reporting(0);
- $ret = mb_strtoupper($string, String::_mbstringCharset($charset));
- error_reporting($old_error);
- if (!empty($ret)) {
- return $ret;
- }
- }
- return strtoupper($string);
- }
-
- if (!isset($uppers)) {
- $uppers = array();
- }
- if (!isset($uppers[$string])) {
- $language = setlocale(LC_CTYPE, 0);
- setlocale(LC_CTYPE, 'C');
- $uppers[$string] = strtoupper($string);
- setlocale(LC_CTYPE, $language);
- }
-
- return $uppers[$string];
- }
-
- /**
- * Returns a string with the first letter capitalized if it is
- * alphabetic.
- *
- * @param string $string The string to be capitalized.
- * @param boolean $locale If true the string will be converted based on a
- * given charset, locale independent else.
- * @param string $charset The charset to use, defaults to current charset.
- *
- * @return string The capitalized string.
- */
- function ucfirst($string, $locale = false, $charset = null)
- {
- if ($locale) {
- $first = String::substr($string, 0, 1, $charset);
- if (String::isAlpha($first, $charset)) {
- $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset);
- }
- } else {
- $string = String::upper(substr($string, 0, 1), false) . substr($string, 1);
- }
- return $string;
- }
-
- /**
- * Returns part of a string.
- *
- * @param string $string The string to be converted.
- * @param integer $start The part's start position, zero based.
- * @param integer $length The part's length.
- * @param string $charset The charset to use when calculating the part's
- * position and length, defaults to current
- * charset.
- *
- * @return string The string's part.
- */
- function substr($string, $start, $length = null, $charset = null)
- {
- if (is_null($length)) {
- $length = String::length($string, $charset) - $start;
- }
-
- if ($length == 0) {
- return '';
- }
-
- /* Try iconv. */
- if (function_exists('iconv_substr')) {
- if (is_null($charset)) {
- $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
-
- $old_error = error_reporting(0);
- $ret = iconv_substr($string, $start, $length, $charset);
- error_reporting($old_error);
- /* iconv_substr() returns false on failure. */
- if ($ret !== false) {
- return $ret;
- }
- }
-
- /* Try mbstring. */
- if (String::extensionExists('mbstring')) {
- if (is_null($charset)) {
- $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
- $old_error = error_reporting(0);
- $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset));
- error_reporting($old_error);
- /* mb_substr() returns empty string on failure. */
- if (strlen($ret)) {
- return $ret;
- }
- }
-
- return substr($string, $start, $length);
- }
-
- /**
- * Returns the character (not byte) length of a string.
- *
- * @param string $string The string to return the length of.
- * @param string $charset The charset to use when calculating the string's
- * length.
- *
- * @return string The string's part.
- */
- function length($string, $charset = null)
- {
- if (is_null($charset)) {
- $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
- $charset = String::lower($charset);
- if ($charset == 'utf-8' || $charset == 'utf8') {
- return strlen(utf8_decode($string));
- }
- if (String::extensionExists('mbstring')) {
- $old_error = error_reporting(0);
- $ret = mb_strlen($string, String::_mbstringCharset($charset));
- error_reporting($old_error);
- if (!empty($ret)) {
- return $ret;
- }
- }
- return strlen($string);
- }
-
- /**
- * Returns the numeric position of the first occurrence of $needle
- * in the $haystack string.
- *
- * @param string $haystack The string to search through.
- * @param string $needle The string to search for.
- * @param integer $offset Allows to specify which character in haystack
- * to start searching.
- * @param string $charset The charset to use when searching for the
- * $needle string.
- *
- * @return integer The position of first occurrence.
- */
- function pos($haystack, $needle, $offset = 0, $charset = null)
- {
- if (String::extensionExists('mbstring')) {
- if (is_null($charset)) {
- $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
- $track_errors = ini_set('track_errors', 1);
- $old_error = error_reporting(0);
- $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset));
- error_reporting($old_error);
- ini_set('track_errors', $track_errors);
- if (!isset($php_errormsg)) {
- return $ret;
- }
- }
- return strpos($haystack, $needle, $offset);
- }
-
- /**
- * Returns a string padded to a certain length with another string.
- *
- * This method behaves exactly like str_pad but is multibyte safe.
- *
- * @param string $input The string to be padded.
- * @param integer $length The length of the resulting string.
- * @param string $pad The string to pad the input string with. Must
- * be in the same charset like the input string.
- * @param const $type The padding type. One of STR_PAD_LEFT,
- * STR_PAD_RIGHT, or STR_PAD_BOTH.
- * @param string $charset The charset of the input and the padding
- * strings.
- *
- * @return string The padded string.
- */
- function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT,
- $charset = null)
- {
- $mb_length = String::length($input, $charset);
- $sb_length = strlen($input);
- $pad_length = String::length($pad, $charset);
-
- /* Return if we already have the length. */
- if ($mb_length >= $length) {
- return $input;
- }
-
- /* Shortcut for single byte strings. */
- if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
- return str_pad($input, $length, $pad, $type);
- }
-
- switch ($type) {
- case STR_PAD_LEFT:
- $left = $length - $mb_length;
- $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input;
- break;
- case STR_PAD_BOTH:
- $left = floor(($length - $mb_length) / 2);
- $right = ceil(($length - $mb_length) / 2);
- $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) .
- $input .
- String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
- break;
- case STR_PAD_RIGHT:
- $right = $length - $mb_length;
- $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
- break;
- }
-
- return $output;
- }
-
- /**
- * Wraps the text of a message.
- *
- * @since Horde 3.2
- *
- * @param string $string String containing the text to wrap.
- * @param integer $width Wrap the string at this number of
- * characters.
- * @param string $break Character(s) to use when breaking lines.
- * @param boolean $cut Whether to cut inside words if a line
- * can't be wrapped.
- * @param string $charset Character set to use when breaking lines.
- * @param boolean $line_folding Whether to apply line folding rules per
- * RFC 822 or similar. The correct break
- * characters including leading whitespace
- * have to be specified too.
- *
- * @return string String containing the wrapped text.
- */
- function wordwrap($string, $width = 75, $break = "\n", $cut = false,
- $charset = null, $line_folding = false)
- {
- /* Get the user's default character set if none passed in. */
- if (is_null($charset)) {
- $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
- }
- $charset = String::_mbstringCharset($charset);
- $string = String::convertCharset($string, $charset, 'utf-8');
- $wrapped = '';
-
- while (String::length($string, 'utf-8') > $width) {
- $line = String::substr($string, 0, $width, 'utf-8');
- $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8');
- // Make sure didn't cut a word, unless we want hard breaks anyway.
- if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) {
- $line .= $match[1];
- $string = $match[2];
- }
- // Wrap at existing line breaks.
- if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) {
- $wrapped .= $match[1] . $match[2];
- $string = $match[3] . $string;
- continue;
- }
- // Wrap at the last colon or semicolon followed by a whitespace if
- // doing line folding.
- if ($line_folding &&
- preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) {
- $wrapped .= $match[1] . $match[2] . $break;
- $string = $match[3] . $string;
- continue;
- }
- // Wrap at the last whitespace of $line.
- if ($line_folding) {
- $sub = '(.+[^\s])';
- } else {
- $sub = '(.*)';
- }
- if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) {
- $wrapped .= $match[1] . $break;
- $string = ($line_folding ? $match[2] : '') . $match[3] . $string;
- continue;
- }
- // Hard wrap if necessary.
- if ($cut) {
- $wrapped .= $line . $break;
- continue;
- }
- $wrapped .= $line;
- }
-
- return String::convertCharset($wrapped . $string, 'utf-8', $charset);
- }
-
- /**
- * Wraps the text of a message.
- *
- * @param string $text String containing the text to wrap.
- * @param integer $length Wrap $text at this number of characters.
- * @param string $break_char Character(s) to use when breaking lines.
- * @param string $charset Character set to use when breaking lines.
- * @param boolean $quote Ignore lines that are wrapped with the '>'
- * character (RFC 2646)? If true, we don't
- * remove any padding whitespace at the end of
- * the string.
- *
- * @return string String containing the wrapped text.
- */
- function wrap($text, $length = 80, $break_char = "\n", $charset = null,
- $quote = false)
- {
- $paragraphs = array();
-
- foreach (preg_split('/\r?\n/', $text) as $input) {
- if ($quote && (strpos($input, '>') === 0)) {
- $line = $input;
- } else {
- /* We need to handle the Usenet-style signature line
- * separately; since the space after the two dashes is
- * REQUIRED, we don't want to trim the line. */
- if ($input != '-- ') {
- $input = rtrim($input);
- }
- $line = String::wordwrap($input, $length, $break_char, false, $charset);
- }
-
- $paragraphs[] = $line;
- }
-
- return implode($break_char, $paragraphs);
- }
-
- /**
- * Returns true if the every character in the parameter is an alphabetic
- * character.
- *
- * @param $string The string to test.
- * @param $charset The charset to use when testing the string.
- *
- * @return boolean True if the parameter was alphabetic only.
- */
- function isAlpha($string, $charset = null)
- {
- if (!String::extensionExists('mbstring')) {
- return ctype_alpha($string);
- }
-
- $charset = String::_mbstringCharset($charset);
- $old_charset = mb_regex_encoding();
- $old_error = error_reporting(0);
-
- if ($charset != $old_charset) {
- mb_regex_encoding($charset);
- }
- $alpha = !mb_ereg_match('[^[:alpha:]]', $string);
- if ($charset != $old_charset) {
- mb_regex_encoding($old_charset);
- }
-
- error_reporting($old_error);
-
- return $alpha;
- }
-
- /**
- * Returns true if ever character in the parameter is a lowercase letter in
- * the current locale.
- *
- * @param $string The string to test.
- * @param $charset The charset to use when testing the string.
- *
- * @return boolean True if the parameter was lowercase.
- */
- function isLower($string, $charset = null)
- {
- return ((String::lower($string, true, $charset) === $string) &&
- String::isAlpha($string, $charset));
- }
-
- /**
- * Returns true if every character in the parameter is an uppercase letter
- * in the current locale.
- *
- * @param string $string The string to test.
- * @param string $charset The charset to use when testing the string.
- *
- * @return boolean True if the parameter was uppercase.
- */
- function isUpper($string, $charset = null)
- {
- return ((String::upper($string, true, $charset) === $string) &&
- String::isAlpha($string, $charset));
- }
-
- /**
- * Performs a multibyte safe regex match search on the text provided.
- *
- * @since Horde 3.1
- *
- * @param string $text The text to search.
- * @param array $regex The regular expressions to use, without perl
- * regex delimiters (e.g. '/' or '|').
- * @param string $charset The character set of the text.
- *
- * @return array The matches array from the first regex that matches.
- */
- function regexMatch($text, $regex, $charset = null)
- {
- if (!empty($charset)) {
- $regex = String::convertCharset($regex, $charset, 'utf-8');
- $text = String::convertCharset($text, $charset, 'utf-8');
- }
-
- $matches = array();
- foreach ($regex as $val) {
- if (preg_match('/' . $val . '/u', $text, $matches)) {
- break;
- }
- }
-
- if (!empty($charset)) {
- $matches = String::convertCharset($matches, 'utf-8', $charset);
- }
-
- return $matches;
- }
-
- /**
- * Workaround charsets that don't work with mbstring functions.
- *
- * @access private
- *
- * @param string $charset The original charset.
- *
- * @return string The charset to use with mbstring functions.
- */
- function _mbstringCharset($charset)
- {
- /* mbstring functions do not handle the 'ks_c_5601-1987' &
- * 'ks_c_5601-1989' charsets. However, these charsets are used, for
- * example, by various versions of Outlook to send Korean characters.
- * Use UHC (CP949) encoding instead. See, e.g.,
- * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */
- if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) {
- $charset = 'UHC';
- }
-
- return $charset;
- }
-
-}
-
-
-
-/**
- * @package Horde_iCalendar
- */
-
-/**
- * String package
- */
-
-
-
-/**
- * Class representing iCalendar files.
- *
- * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @since Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar {
-
- /**
- * The parent (containing) iCalendar object.
- *
- * @var Horde_iCalendar
- */
- var $_container = false;
-
- /**
- * The name/value pairs of attributes for this object (UID,
- * DTSTART, etc.). Which are present depends on the object and on
- * what kind of component it is.
- *
- * @var array
- */
- var $_attributes = array();
-
- /**
- * Any children (contained) iCalendar components of this object.
- *
- * @var array
- */
- var $_components = array();
-
- /**
- * According to RFC 2425, we should always use CRLF-terminated lines.
- *
- * @var string
- */
- var $_newline = "\r\n";
-
- /**
- * iCalendar format version (different behavior for 1.0 and 2.0
- * especially with recurring events).
- *
- * @var string
- */
- var $_version;
-
- function Horde_iCalendar($version = '2.0')
- {
- $this->_version = $version;
- $this->setAttribute('VERSION', $version);
- }
-
- /**
- * Return a reference to a new component.
- *
- * @param string $type The type of component to return
- * @param Horde_iCalendar $container A container that this component
- * will be associated with.
- *
- * @return object Reference to a Horde_iCalendar_* object as specified.
- *
- * @static
- */
- function &newComponent($type, &$container)
- {
- $type = String::lower($type);
- $class = 'Horde_iCalendar_' . $type;
- if (!class_exists($class)) {
- include 'Horde/iCalendar/' . $type . '.php';
- }
- if (class_exists($class)) {
- $component = new $class();
- if ($container !== false) {
- $component->_container = &$container;
- // Use version of container, not default set by component
- // constructor.
- $component->_version = $container->_version;
- }
- } else {
- // Should return an dummy x-unknown type class here.
- $component = false;
- }
-
- return $component;
- }
-
- /**
- * Sets the value of an attribute.
- *
- * @param string $name The name of the attribute.
- * @param string $value The value of the attribute.
- * @param array $params Array containing any addition parameters for
- * this attribute.
- * @param boolean $append True to append the attribute, False to replace
- * the first matching attribute found.
- * @param array $values Array representation of $value. For
- * comma/semicolon seperated lists of values. If
- * not set use $value as single array element.
- */
- function setAttribute($name, $value, $params = array(), $append = true,
- $values = false)
- {
- // Make sure we update the internal format version if
- // setAttribute('VERSION', ...) is called.
- if ($name == 'VERSION') {
- $this->_version = $value;
- if ($this->_container !== false) {
- $this->_container->_version = $value;
- }
- }
-
- if (!$values) {
- $values = array($value);
- }
- $found = false;
- if (!$append) {
- foreach (array_keys($this->_attributes) as $key) {
- if ($this->_attributes[$key]['name'] == String::upper($name)) {
- $this->_attributes[$key]['params'] = $params;
- $this->_attributes[$key]['value'] = $value;
- $this->_attributes[$key]['values'] = $values;
- $found = true;
- break;
- }
- }
- }
-
- if ($append || !$found) {
- $this->_attributes[] = array(
- 'name' => String::upper($name),
- 'params' => $params,
- 'value' => $value,
- 'values' => $values
- );
- }
- }
-
- /**
- * Sets parameter(s) for an (already existing) attribute. The
- * parameter set is merged into the existing set.
- *
- * @param string $name The name of the attribute.
- * @param array $params Array containing any additional parameters for
- * this attribute.
- * @return boolean True on success, false if no attribute $name exists.
- */
- function setParameter($name, $params = array())
- {
- $keys = array_keys($this->_attributes);
- foreach ($keys as $key) {
- if ($this->_attributes[$key]['name'] == $name) {
- $this->_attributes[$key]['params'] =
- array_merge($this->_attributes[$key]['params'], $params);
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Get the value of an attribute.
- *
- * @param string $name The name of the attribute.
- * @param boolean $params Return the parameters for this attribute instead
- * of its value.
- *
- * @return mixed (object) PEAR_Error if the attribute does not exist.
- * (string) The value of the attribute.
- * (array) The parameters for the attribute or
- * multiple values for an attribute.
- */
- function getAttribute($name, $params = false)
- {
- $result = array();
- foreach ($this->_attributes as $attribute) {
- if ($attribute['name'] == $name) {
- if ($params) {
- $result[] = $attribute['params'];
- } else {
- $result[] = $attribute['value'];
- }
- }
- }
- if (!count($result)) {
- require_once 'PEAR.php';
- return PEAR::raiseError('Attribute "' . $name . '" Not Found');
- } if (count($result) == 1 && !$params) {
- return $result[0];
- } else {
- return $result;
- }
- }
-
- /**
- * Gets the values of an attribute as an array. Multiple values
- * are possible due to:
- *
- * a) multiplce occurences of 'name'
- * b) (unsecapd) comma seperated lists.
- *
- * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
- * will return array('a', 'b', 'c').
- *
- * @param string $name The name of the attribute.
- * @return mixed (object) PEAR_Error if the attribute does not exist.
- * (array) Multiple values for an attribute.
- */
- function getAttributeValues($name)
- {
- $result = array();
- foreach ($this->_attributes as $attribute) {
- if ($attribute['name'] == $name) {
- $result = array_merge($attribute['values'], $result);
- }
- }
- if (!count($result)) {
- return PEAR::raiseError('Attribute "' . $name . '" Not Found');
- }
- return $result;
- }
-
- /**
- * Returns the value of an attribute, or a specified default value
- * if the attribute does not exist.
- *
- * @param string $name The name of the attribute.
- * @param mixed $default What to return if the attribute specified by
- * $name does not exist.
- *
- * @return mixed (string) The value of $name.
- * (mixed) $default if $name does not exist.
- */
- function getAttributeDefault($name, $default = '')
- {
- $value = $this->getAttribute($name);
- return is_a($value, 'PEAR_Error') ? $default : $value;
- }
-
- /**
- * Remove all occurences of an attribute.
- *
- * @param string $name The name of the attribute.
- */
- function removeAttribute($name)
- {
- $keys = array_keys($this->_attributes);
- foreach ($keys as $key) {
- if ($this->_attributes[$key]['name'] == $name) {
- unset($this->_attributes[$key]);
- }
- }
- }
-
- /**
- * Get attributes for all tags or for a given tag.
- *
- * @param string $tag Return attributes for this tag, or all attributes if
- * not given.
- *
- * @return array An array containing all the attributes and their types.
- */
- function getAllAttributes($tag = false)
- {
- if ($tag === false) {
- return $this->_attributes;
- }
- $result = array();
- foreach ($this->_attributes as $attribute) {
- if ($attribute['name'] == $tag) {
- $result[] = $attribute;
- }
- }
- return $result;
- }
-
- /**
- * Add a vCalendar component (eg vEvent, vTimezone, etc.).
- *
- * @param Horde_iCalendar $component Component (subclass) to add.
- */
- function addComponent($component)
- {
- if (is_a($component, 'Horde_iCalendar')) {
- $component->_container = &$this;
- $this->_components[] = &$component;
- }
- }
-
- /**
- * Retrieve all the components.
- *
- * @return array Array of Horde_iCalendar objects.
- */
- function getComponents()
- {
- return $this->_components;
- }
-
- function getType()
- {
- return 'vcalendar';
- }
-
- /**
- * Return the classes (entry types) we have.
- *
- * @return array Hash with class names Horde_iCalendar_xxx as keys
- * and number of components of this class as value.
- */
- function getComponentClasses()
- {
- $r = array();
- foreach ($this->_components as $c) {
- $cn = strtolower(get_class($c));
- if (empty($r[$cn])) {
- $r[$cn] = 1;
- } else {
- $r[$cn]++;
- }
- }
-
- return $r;
- }
-
- /**
- * Number of components in this container.
- *
- * @return integer Number of components in this container.
- */
- function getComponentCount()
- {
- return count($this->_components);
- }
-
- /**
- * Retrieve a specific component.
- *
- * @param integer $idx The index of the object to retrieve.
- *
- * @return mixed (boolean) False if the index does not exist.
- * (Horde_iCalendar_*) The requested component.
- */
- function getComponent($idx)
- {
- if (isset($this->_components[$idx])) {
- return $this->_components[$idx];
- } else {
- return false;
- }
- }
-
- /**
- * Locates the first child component of the specified class, and returns a
- * reference to it.
- *
- * @param string $type The type of component to find.
- *
- * @return boolean|Horde_iCalendar_* False if no subcomponent of the
- * specified class exists or a reference
- * to the requested component.
- */
- function &findComponent($childclass)
- {
- $childclass = 'Horde_iCalendar_' . String::lower($childclass);
- $keys = array_keys($this->_components);
- foreach ($keys as $key) {
- if (is_a($this->_components[$key], $childclass)) {
- return $this->_components[$key];
- }
- }
-
- $component = false;
- return $component;
- }
-
- /**
- * Locates the first matching child component of the specified class, and
- * returns a reference to it.
- *
- * @param string $childclass The type of component to find.
- * @param string $attribute This attribute must be set in the component
- * for it to match.
- * @param string $value Optional value that $attribute must match.
- *
- * @return boolean|Horde_iCalendar_* False if no matching subcomponent of
- * the specified class exists, or a
- * reference to the requested component.
- */
- function &findComponentByAttribute($childclass, $attribute, $value = null)
- {
- $childclass = 'Horde_iCalendar_' . String::lower($childclass);
- $keys = array_keys($this->_components);
- foreach ($keys as $key) {
- if (is_a($this->_components[$key], $childclass)) {
- $attr = $this->_components[$key]->getAttribute($attribute);
- if (is_a($attr, 'PEAR_Error')) {
- continue;
- }
- if ($value !== null && $value != $attr) {
- continue;
- }
- return $this->_components[$key];
- }
- }
-
- $component = false;
- return $component;
- }
-
- /**
- * Clears the iCalendar object (resets the components and attributes
- * arrays).
- */
- function clear()
- {
- $this->_components = array();
- $this->_attributes = array();
- }
-
- /**
- * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
- *
- * These 'old' formats are defined by www.imc.org. The 'new' (non-old)
- * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
- * respectively.
- *
- * @since Horde 3.1.2
- */
- function isOldFormat()
- {
- if ($this->getType() == 'vcard') {
- return ($this->_version < 3);
- }
- if ($this->getType() == 'vNote') {
- return ($this->_version < 2);
- }
- if ($this->_version >= 2) {
- return false;
- }
- return true;
- }
-
- /**
- * Export as vCalendar format.
- */
- function exportvCalendar()
- {
- // Default values.
- $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
- $requiredAttributes['METHOD'] = 'PUBLISH';
-
- foreach ($requiredAttributes as $name => $default_value) {
- if (is_a($this->getattribute($name), 'PEAR_Error')) {
- $this->setAttribute($name, $default_value);
- }
- }
-
- return $this->_exportvData('VCALENDAR');
- }
-
- /**
- * Export this entry as a hash array with tag names as keys.
- *
- * @param boolean $paramsInKeys
- * If false, the operation can be quite lossy as the
- * parameters are ignored when building the array keys.
- * So if you export a vcard with
- * LABEL;TYPE=WORK:foo
- * LABEL;TYPE=HOME:bar
- * the resulting hash contains only one label field!
- * If set to true, array keys look like 'LABEL;TYPE=WORK'
- * @return array A hash array with tag names as keys.
- */
- function toHash($paramsInKeys = false)
- {
- $hash = array();
- foreach ($this->_attributes as $a) {
- $k = $a['name'];
- if ($paramsInKeys && is_array($a['params'])) {
- foreach ($a['params'] as $p => $v) {
- $k .= ";$p=$v";
- }
- }
- $hash[$k] = $a['value'];
- }
-
- return $hash;
- }
-
- /**
- * Parses a string containing vCalendar data.
- *
- * @todo This method doesn't work well at all, if $base is VCARD.
- *
- * @param string $text The data to parse.
- * @param string $base The type of the base object.
- * @param string $charset The encoding charset for $text. Defaults to
- * utf-8 for new format, iso-8859-1 for old format.
- * @param boolean $clear If true clears the iCal object before parsing.
- *
- * @return boolean True on successful import, false otherwise.
- */
- function parsevCalendar($text, $base = 'VCALENDAR', $charset = null,
- $clear = true)
- {
- if ($clear) {
- $this->clear();
- }
- if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
- $container = true;
- $vCal = $matches[1];
- } else {
- // Text isn't enclosed in BEGIN:VCALENDAR
- // .. END:VCALENDAR. We'll try to parse it anyway.
- $container = false;
- $vCal = $text;
- }
- $vCal = trim($vCal);
-
- // Extract all subcomponents.
- $matches = $components = null;
- if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) {
- foreach ($components[0] as $key => $data) {
- // Remove from the vCalendar data.
- $vCal = str_replace($data, '', $vCal);
- }
- } elseif (!$container) {
- return false;
- }
-
- // Unfold "quoted printable" folded lines like:
- // BODY;ENCODING=QUOTED-PRINTABLE:=
- // another=20line=
- // last=20line
- while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) {
- foreach ($matches[1] as $s) {
- $r = preg_replace('/=\r?\n/', '', $s);
- $vCal = str_replace($s, $r, $vCal);
- }
- }
-
- // Unfold any folded lines.
- if ($this->isOldFormat()) {
- $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal);
- } else {
- $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
- }
-
- // Parse the remaining attributes.
- if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
- foreach ($matches[0] as $attribute) {
- preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
- $tag = trim(String::upper($parts[1]));
- $value = $parts[4];
- $params = array();
-
- // Parse parameters.
- if (!empty($parts[2])) {
- preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
- foreach ($param_parts[2] as $key => $paramName) {
- $paramName = String::upper($paramName);
- $paramValue = $param_parts[4][$key];
- if ($paramName == 'TYPE') {
- $paramValue = preg_split('/(?<!\\\\),/', $paramValue);
- if (count($paramValue) == 1) {
- $paramValue = $paramValue[0];
- }
- }
- if (is_string($paramValue)) {
- if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
- $paramValue = $parts[1];
- }
- } else {
- foreach ($paramValue as $k => $tmp) {
- if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
- $paramValue[$k] = $parts[1];
- }
- }
- }
- $params[$paramName] = $paramValue;
- }
- }
-
- // Charset and encoding handling.
- if ((isset($params['ENCODING']) &&
- String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
- isset($params['QUOTED-PRINTABLE'])) {
-
- $value = quoted_printable_decode($value);
- if (isset($params['CHARSET'])) {
- $value = String::convertCharset($value, $params['CHARSET']);
- } else {
- $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
- }
- } elseif (isset($params['CHARSET'])) {
- $value = String::convertCharset($value, $params['CHARSET']);
- } else {
- // As per RFC 2279, assume UTF8 if we don't have an
- // explicit charset parameter.
- $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
- }
-
- // Get timezone info for date fields from $params.
- $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
-
- switch ($tag) {
- // Date fields.
- case 'COMPLETED':
- case 'CREATED':
- case 'LAST-MODIFIED':
- case 'X-MOZ-LASTACK':
- case 'X-MOZ-SNOOZE-TIME':
- $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
- break;
-
- case 'BDAY':
- case 'X-SYNCJE-ANNIVERSARY':
- case 'X-ANNIVERSARY':
- $this->setAttribute($tag, $this->_parseDate($value), $params);
- break;
-
- case 'DTEND':
- case 'DTSTART':
- case 'DTSTAMP':
- case 'DUE':
- case 'AALARM':
- case 'RECURRENCE-ID':
- // types like AALARM may contain additional data after a ;
- // ignore these.
- $ts = explode(';', $value);
- if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
- $this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
- } else {
- $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
- }
- break;
-
- case 'TRIGGER':
- if (isset($params['VALUE']) &&
- $params['VALUE'] == 'DATE-TIME') {
- $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
- } else {
- $this->setAttribute($tag, $this->_parseDuration($value), $params);
- }
- break;
-
- // Comma seperated dates.
- case 'EXDATE':
- case 'RDATE':
- if (!strlen($value)) {
- break;
- }
- $dates = array();
- $separator = $this->isOldFormat() ? ';' : ',';
- preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
-
- foreach ($values[1] as $value) {
- $dates[] = $this->_parseDate($value);
- }
- $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
- break;
-
- // Duration fields.
- case 'DURATION':
- $this->setAttribute($tag, $this->_parseDuration($value), $params);
- break;
-
- // Period of time fields.
- case 'FREEBUSY':
- $periods = array();
- preg_match_all('/,([^,]*)/', ',' . $value, $values);
- foreach ($values[1] as $value) {
- $periods[] = $this->_parsePeriod($value);
- }
-
- $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
- break;
-
- // UTC offset fields.
- case 'TZOFFSETFROM':
- case 'TZOFFSETTO':
- $this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
- break;
-
- // Integer fields.
- case 'PERCENT-COMPLETE':
- case 'PRIORITY':
- case 'REPEAT':
- case 'SEQUENCE':
- $this->setAttribute($tag, intval($value), $params);
- break;
-
- // Geo fields.
- case 'GEO':
- if ($this->isOldFormat()) {
- $floats = explode(',', $value);
- $value = array('latitude' => floatval($floats[1]),
- 'longitude' => floatval($floats[0]));
- } else {
- $floats = explode(';', $value);
- $value = array('latitude' => floatval($floats[0]),
- 'longitude' => floatval($floats[1]));
- }
- $this->setAttribute($tag, $value, $params);
- break;
-
- // Recursion fields.
- case 'EXRULE':
- case 'RRULE':
- $this->setAttribute($tag, trim($value), $params);
- break;
-
- // ADR, ORG and N are lists seperated by unescaped semicolons
- // with a specific number of slots.
- case 'ADR':
- case 'N':
- case 'ORG':
- $value = trim($value);
- // As of rfc 2426 2.4.2 semicolon, comma, and colon must
- // be escaped (comma is unescaped after splitting below).
- $value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
- array($this->_newline, $this->_newline, ';', ':'),
- $value);
-
- // Split by unescaped semicolons:
- $values = preg_split('/(?<!\\\\);/', $value);
- $value = str_replace('\\;', ';', $value);
- $values = str_replace('\\;', ';', $values);
- $this->setAttribute($tag, trim($value), $params, true, $values);
- break;
-
- // String fields.
- default:
- if ($this->isOldFormat()) {
- // vCalendar 1.0 and vCard 2.1 only escape semicolons
- // and use unescaped semicolons to create lists.
- $value = trim($value);
- // Split by unescaped semicolons:
- $values = preg_split('/(?<!\\\\);/', $value);
- $value = str_replace('\\;', ';', $value);
- $values = str_replace('\\;', ';', $values);
- $this->setAttribute($tag, trim($value), $params, true, $values);
- } else {
- $value = trim($value);
- // As of rfc 2426 2.4.2 semicolon, comma, and colon
- // must be escaped (comma is unescaped after splitting
- // below).
- $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
- array($this->_newline, $this->_newline, ';', ':', '\\'),
- $value);
-
- // Split by unescaped commas.
- $values = preg_split('/(?<!\\\\),/', $value);
- $value = str_replace('\\,', ',', $value);
- $values = str_replace('\\,', ',', $values);
-
- $this->setAttribute($tag, trim($value), $params, true, $values);
- }
- break;
- }
- }
- }
-
- // Process all components.
- if ($components) {
- // vTimezone components are processed first. They are
- // needed to process vEvents that may use a TZID.
- foreach ($components[0] as $key => $data) {
- $type = trim($components[1][$key]);
- if ($type != 'VTIMEZONE') {
- continue;
- }
- $component = &Horde_iCalendar::newComponent($type, $this);
- if ($component === false) {
- return PEAR::raiseError("Unable to create object for type $type");
- }
- $component->parsevCalendar($data, $type, $charset);
-
- $this->addComponent($component);
- }
-
- // Now process the non-vTimezone components.
- foreach ($components[0] as $key => $data) {
- $type = trim($components[1][$key]);
- if ($type == 'VTIMEZONE') {
- continue;
- }
- $component = &Horde_iCalendar::newComponent($type, $this);
- if ($component === false) {
- return PEAR::raiseError("Unable to create object for type $type");
- }
- $component->parsevCalendar($data, $type, $charset);
-
- $this->addComponent($component);
- }
- }
-
- return true;
- }
-
- /**
- * Export this component in vCal format.
- *
- * @param string $base The type of the base object.
- *
- * @return string vCal format data.
- */
- function _exportvData($base = 'VCALENDAR')
- {
- $result = 'BEGIN:' . String::upper($base) . $this->_newline;
-
- // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
- // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
- if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
- $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') {
- // Ensure that version is the first attribute.
- $result .= 'VERSION:' . $this->_version . $this->_newline;
- }
- foreach ($this->_attributes as $attribute) {
- $name = $attribute['name'];
- if ($name == 'VERSION') {
- // Already done.
- continue;
- }
-
- $params_str = '';
- $params = $attribute['params'];
- if ($params) {
- foreach ($params as $param_name => $param_value) {
- /* Skip CHARSET for iCalendar 2.0 data, not allowed. */
- if ($param_name == 'CHARSET' && !$this->isOldFormat()) {
- continue;
- }
- /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
- if ($this->isOldFormat() &&
- $param_name == 'VALUE' && $param_value == 'DATE') {
- continue;
- }
-
- if ($param_value === null) {
- $params_str .= ";$param_name";
- } else {
- $len = strlen($param_value);
- $safe_value = '';
- $quote = false;
- for ($i = 0; $i < $len; ++$i) {
- $ord = ord($param_value[$i]);
- // Accept only valid characters.
- if ($ord == 9 || $ord == 32 || $ord == 33 ||
- ($ord >= 35 && $ord <= 126) ||
- $ord >= 128) {
- $safe_value .= $param_value[$i];
- // Characters above 128 do not need to be
- // quoted as per RFC2445 but Outlook requires
- // this.
- if ($ord == 44 || $ord == 58 || $ord == 59 ||
- $ord >= 128) {
- $quote = true;
- }
- }
- }
- if ($quote) {
- $safe_value = '"' . $safe_value . '"';
- }
- $params_str .= ";$param_name=$safe_value";
- }
- }
- }
-
- $value = $attribute['value'];
- switch ($name) {
- // Date fields.
- case 'COMPLETED':
- case 'CREATED':
- case 'DCREATED':
- case 'LAST-MODIFIED':
- case 'X-MOZ-LASTACK':
- case 'X-MOZ-SNOOZE-TIME':
- $value = $this->_exportDateTime($value);
- break;
-
- case 'DTEND':
- case 'DTSTART':
- case 'DTSTAMP':
- case 'DUE':
- case 'AALARM':
- case 'RECURRENCE-ID':
- if (isset($params['VALUE'])) {
- if ($params['VALUE'] == 'DATE') {
- // VCALENDAR 1.0 uses T000000 - T235959 for all day events:
- if ($this->isOldFormat() && $name == 'DTEND') {
- $d = new Horde_Date($value);
- $value = new Horde_Date(array(
- 'year' => $d->year,
- 'month' => $d->month,
- 'mday' => $d->mday - 1));
- $value->correct();
- $value = $this->_exportDate($value, '235959');
- } else {
- $value = $this->_exportDate($value, '000000');
- }
- } else {
- $value = $this->_exportDateTime($value);
- }
- } else {
- $value = $this->_exportDateTime($value);
- }
- break;
-
- // Comma seperated dates.
- case 'EXDATE':
- case 'RDATE':
- $dates = array();
- foreach ($value as $date) {
- if (isset($params['VALUE'])) {
- if ($params['VALUE'] == 'DATE') {
- $dates[] = $this->_exportDate($date, '000000');
- } elseif ($params['VALUE'] == 'PERIOD') {
- $dates[] = $this->_exportPeriod($date);
- } else {
- $dates[] = $this->_exportDateTime($date);
- }
- } else {
- $dates[] = $this->_exportDateTime($date);
- }
- }
- $value = implode($this->isOldFormat() ? ';' : ',', $dates);
- break;
-
- case 'TRIGGER':
- if (isset($params['VALUE'])) {
- if ($params['VALUE'] == 'DATE-TIME') {
- $value = $this->_exportDateTime($value);
- } elseif ($params['VALUE'] == 'DURATION') {
- $value = $this->_exportDuration($value);
- }
- } else {
- $value = $this->_exportDuration($value);
- }
- break;
-
- // Duration fields.
- case 'DURATION':
- $value = $this->_exportDuration($value);
- break;
-
- // Period of time fields.
- case 'FREEBUSY':
- $value_str = '';
- foreach ($value as $period) {
- $value_str .= empty($value_str) ? '' : ',';
- $value_str .= $this->_exportPeriod($period);
- }
- $value = $value_str;
- break;
-
- // UTC offset fields.
- case 'TZOFFSETFROM':
- case 'TZOFFSETTO':
- $value = $this->_exportUtcOffset($value);
- break;
-
- // Integer fields.
- case 'PERCENT-COMPLETE':
- case 'PRIORITY':
- case 'REPEAT':
- case 'SEQUENCE':
- $value = "$value";
- break;
-
- // Geo fields.
- case 'GEO':
- if ($this->isOldFormat()) {
- $value = $value['longitude'] . ',' . $value['latitude'];
- } else {
- $value = $value['latitude'] . ';' . $value['longitude'];
- }
- break;
-
- // Recurrence fields.
- case 'EXRULE':
- case 'RRULE':
- break;
-
- default:
- if ($this->isOldFormat()) {
- if (is_array($attribute['values']) &&
- count($attribute['values']) > 1) {
- $values = $attribute['values'];
- if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
- $glue = ';';
- } else {
- $glue = ',';
- }
- $values = str_replace(';', '\\;', $values);
- $value = implode($glue, $values);
- } else {
- /* vcard 2.1 and vcalendar 1.0 escape only
- * semicolons */
- $value = str_replace(';', '\\;', $value);
- }
- // Text containing newlines or ASCII >= 127 must be BASE64
- // or QUOTED-PRINTABLE encoded. Currently we use
- // QUOTED-PRINTABLE as default.
- if (preg_match("/[^\x20-\x7F]/", $value) &&
- empty($params['ENCODING'])) {
- $params['ENCODING'] = 'QUOTED-PRINTABLE';
- $params_str .= ';ENCODING=QUOTED-PRINTABLE';
- // Add CHARSET as well. At least the synthesis client
- // gets confused otherwise
- if (empty($params['CHARSET'])) {
- $params['CHARSET'] = 'UTF-8';
- $params_str .= ';CHARSET=' . $params['CHARSET'];
- }
- }
- } else {
- if (is_array($attribute['values']) &&
- count($attribute['values'])) {
- $values = $attribute['values'];
- if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
- $glue = ';';
- } else {
- $glue = ',';
- }
- // As of rfc 2426 2.5 semicolon and comma must be
- // escaped.
- $values = str_replace(array('\\', ';', ','),
- array('\\\\', '\\;', '\\,'),
- $values);
- $value = implode($glue, $values);
- } else {
- // As of rfc 2426 2.5 semicolon and comma must be
- // escaped.
- $value = str_replace(array('\\', ';', ','),
- array('\\\\', '\\;', '\\,'),
- $value);
- }
- $value = preg_replace('/\r?\n/', '\n', $value);
- }
- break;
- }
-
- $value = str_replace("\r", '', $value);
- if (!empty($params['ENCODING']) &&
- $params['ENCODING'] == 'QUOTED-PRINTABLE' &&
- strlen(trim($value))) {
- $result .= $name . $params_str . ':'
- . str_replace('=0A', '=0D=0A',
- $this->_quotedPrintableEncode($value))
- . $this->_newline;
- } else {
- $attr_string = $name . $params_str . ':' . $value;
- if (!$this->isOldFormat()) {
- $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ',
- true, 'utf-8', true);
- }
- $result .= $attr_string . $this->_newline;
- }
- }
-
- foreach ($this->_components as $component) {
- $result .= $component->exportvCalendar();
- }
-
- return $result . 'END:' . $base . $this->_newline;
- }
-
- /**
- * Parse a UTC Offset field.
- */
- function _parseUtcOffset($text)
- {
- $offset = array();
- if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
- $offset['ahead'] = (bool)($timeParts[1] == '+');
- $offset['hour'] = intval($timeParts[2]);
- $offset['minute'] = intval($timeParts[3]);
- if (isset($timeParts[4])) {
- $offset['second'] = intval($timeParts[4]);
- }
- return $offset;
- } else {
- return false;
- }
- }
-
- /**
- * Export a UTC Offset field.
- */
- function _exportUtcOffset($value)
- {
- $offset = $value['ahead'] ? '+' : '-';
- $offset .= sprintf('%02d%02d',
- $value['hour'], $value['minute']);
- if (isset($value['second'])) {
- $offset .= sprintf('%02d', $value['second']);
- }
-
- return $offset;
- }
-
- /**
- * Parse a Time Period field.
- */
- function _parsePeriod($text)
- {
- $periodParts = explode('/', $text);
-
- $start = $this->_parseDateTime($periodParts[0]);
-
- if ($duration = $this->_parseDuration($periodParts[1])) {
- return array('start' => $start, 'duration' => $duration);
- } elseif ($end = $this->_parseDateTime($periodParts[1])) {
- return array('start' => $start, 'end' => $end);
- }
- }
-
- /**
- * Export a Time Period field.
- */
- function _exportPeriod($value)
- {
- $period = $this->_exportDateTime($value['start']);
- $period .= '/';
- if (isset($value['duration'])) {
- $period .= $this->_exportDuration($value['duration']);
- } else {
- $period .= $this->_exportDateTime($value['end']);
- }
- return $period;
- }
-
- /**
- * Grok the TZID and return an offset in seconds from UTC for this
- * date and time.
- */
- function _parseTZID($date, $time, $tzid)
- {
- $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
- if (!$vtimezone) {
- // use PHP's standard timezone db to determine tzoffset
- try {
- $tz = new DateTimeZone($tzid);
- $dt = new DateTime('now', $tz);
- $dt->setDate($date['year'], $date['month'], $date['mday']);
- $dt->setTime($time['hour'], $time['minute'], $date['recond']);
- return $tz->getOffset($dt);
- }
- catch (Exception $e) {
- return false;
- }
- }
-
- $change_times = array();
- foreach ($vtimezone->getComponents() as $o) {
- $t = $vtimezone->parseChild($o, $date['year']);
- if ($t !== false) {
- $change_times[] = $t;
- }
- }
-
- if (!$change_times) {
- return false;
- }
-
- sort($change_times);
-
- // Time is arbitrarily based on UTC for comparison.
- $t = @gmmktime($time['hour'], $time['minute'], $time['second'],
- $date['month'], $date['mday'], $date['year']);
-
- if ($t < $change_times[0]['time']) {
- return $change_times[0]['from'];
- }
-
- for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
- if (($t >= $change_times[$i]['time']) &&
- ($t < $change_times[$i + 1]['time'])) {
- return $change_times[$i]['to'];
- }
- }
-
- if ($t >= $change_times[$n - 1]['time']) {
- return $change_times[$n - 1]['to'];
- }
-
- return false;
- }
-
- /**
- * Parses a DateTime field and returns a unix timestamp. If the
- * field cannot be parsed then the original text is returned
- * unmodified.
- *
- * @todo This function should be moved to Horde_Date and made public.
- */
- function _parseDateTime($text, $tzid = false)
- {
- $dateParts = explode('T', $text);
- if (count($dateParts) != 2 && !empty($text)) {
- // Not a datetime field but may be just a date field.
- if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
- // Or not
- return $text;
- }
- $newtext = $text.'T000000';
- $dateParts = explode('T', $newtext);
- }
-
- if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) {
- return $text;
- }
- if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) {
- return $text;
- }
-
- // Get timezone info for date fields from $tzid and container.
- $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar'))
- ? $this->_parseTZID($date, $time, $tzid) : false;
- if ($time['zone'] == 'UTC' || $tzoffset !== false) {
- $result = @gmmktime($time['hour'], $time['minute'], $time['second'],
- $date['month'], $date['mday'], $date['year']);
- if ($tzoffset) {
- $result -= $tzoffset;
- }
- } else {
- // We don't know the timezone so assume local timezone.
- // FIXME: shouldn't this be based on the user's timezone
- // preference rather than the server's timezone?
- $result = @mktime($time['hour'], $time['minute'], $time['second'],
- $date['month'], $date['mday'], $date['year']);
- }
-
- return ($result !== false) ? $result : $text;
- }
-
- /**
- * Export a DateTime field.
- */
- function _exportDateTime($value)
- {
- $temp = array();
- if (!is_object($value) && !is_array($value)) {
- $tz = date('O', $value);
- $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2));
- $value -= $TZOffset;
-
- $temp['zone'] = 'UTC';
- list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value));
- } else {
- $dateOb = new Horde_Date($value);
- return Horde_iCalendar::_exportDateTime($dateOb->timestamp());
- }
-
- return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp);
- }
-
- /**
- * Parses a Time field.
- *
- * @static
- */
- function _parseTime($text)
- {
- if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
- $time['hour'] = intval($timeParts[1]);
- $time['minute'] = intval($timeParts[2]);
- $time['second'] = intval($timeParts[3]);
- if (isset($timeParts[4])) {
- $time['zone'] = 'UTC';
- } else {
- $time['zone'] = 'Local';
- }
- return $time;
- } else {
- return false;
- }
- }
-
- /**
- * Exports a Time field.
- */
- function _exportTime($value)
- {
- $time = sprintf('%02d%02d%02d',
- $value['hour'], $value['minute'], $value['second']);
- if ($value['zone'] == 'UTC') {
- $time .= 'Z';
- }
- return $time;
- }
-
- /**
- * Parses a Date field.
- *
- * @static
- */
- function _parseDate($text)
- {
- $parts = explode('T', $text);
- if (count($parts) == 2) {
- $text = $parts[0];
- }
-
- if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
- return false;
- }
-
- return array('year' => $match[1],
- 'month' => $match[2],
- 'mday' => $match[3]);
- }
-
- /**
- * Exports a date field.
- *
- * @param object|array $value Date object or hash.
- * @param string $autoconvert If set, use this as time part to export the
- * date as datetime when exporting to Vcalendar
- * 1.0. Examples: '000000' or '235959'
- */
- function _exportDate($value, $autoconvert = false)
- {
- if (is_object($value)) {
- $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
- }
- if ($autoconvert !== false && $this->isOldFormat()) {
- return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert);
- } else {
- return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
- }
- }
-
- /**
- * Parse a Duration Value field.
- */
- function _parseDuration($text)
- {
- if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
- // Weeks.
- $duration = 7 * 86400 * intval($durvalue[3]);
-
- if (count($durvalue) > 4) {
- // Days.
- $duration += 86400 * intval($durvalue[4]);
- }
- if (count($durvalue) > 5) {
- // Hours.
- $duration += 3600 * intval($durvalue[7]);
-
- // Mins.
- if (isset($durvalue[8])) {
- $duration += 60 * intval($durvalue[8]);
- }
-
- // Secs.
- if (isset($durvalue[9])) {
- $duration += intval($durvalue[9]);
- }
- }
-
- // Sign.
- if ($durvalue[1] == "-") {
- $duration *= -1;
- }
-
- return $duration;
- } else {
- return false;
- }
- }
-
- /**
- * Export a duration value.
- */
- function _exportDuration($value)
- {
- $duration = '';
- if ($value < 0) {
- $value *= -1;
- $duration .= '-';
- }
- $duration .= 'P';
-
- $weeks = floor($value / (7 * 86400));
- $value = $value % (7 * 86400);
- if ($weeks) {
- $duration .= $weeks . 'W';
- }
-
- $days = floor($value / (86400));
- $value = $value % (86400);
- if ($days) {
- $duration .= $days . 'D';
- }
-
- if ($value) {
- $duration .= 'T';
-
- $hours = floor($value / 3600);
- $value = $value % 3600;
- if ($hours) {
- $duration .= $hours . 'H';
- }
-
- $mins = floor($value / 60);
- $value = $value % 60;
- if ($mins) {
- $duration .= $mins . 'M';
- }
-
- if ($value) {
- $duration .= $value . 'S';
- }
- }
-
- return $duration;
- }
-
- /**
- * Converts an 8bit string to a quoted-printable string according to RFC
- * 2045, section 6.7.
- *
- * imap_8bit() does not apply all necessary rules.
- *
- * @param string $input The string to be encoded.
- *
- * @return string The quoted-printable encoded string.
- */
- function _quotedPrintableEncode($input = '')
- {
- $output = $line = '';
- $len = strlen($input);
-
- for ($i = 0; $i < $len; ++$i) {
- $ord = ord($input[$i]);
- // Encode non-printable characters (rule 2).
- if ($ord == 9 ||
- ($ord >= 32 && $ord <= 60) ||
- ($ord >= 62 && $ord <= 126)) {
- $chunk = $input[$i];
- } else {
- // Quoted printable encoding (rule 1).
- $chunk = '=' . String::upper(sprintf('%02X', $ord));
- }
- $line .= $chunk;
- // Wrap long lines (rule 5)
- if (strlen($line) + 1 > 76) {
- $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true);
- $newline = strrchr($line, "\r\n");
- if ($newline !== false) {
- $output .= substr($line, 0, -strlen($newline) + 2);
- $line = substr($newline, 2);
- } else {
- $output .= $line;
- }
- continue;
- }
- // Wrap at line breaks for better readability (rule 4).
- if (substr($line, -3) == '=0A') {
- $output .= $line . "=\r\n";
- $line = '';
- }
- }
- $output .= $line;
-
- // Trailing whitespace must be encoded (rule 3).
- $lastpos = strlen($output) - 1;
- if ($output[$lastpos] == chr(9) ||
- $output[$lastpos] == chr(32)) {
- $output[$lastpos] = '=';
- $output .= String::upper(sprintf('%02X', ord($output[$lastpos])));
- }
-
- return $output;
- }
-
-}
-
-
-
-/**
- * Class representing vAlarms.
- *
- * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @since Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_valarm extends Horde_iCalendar {
-
- function getType()
- {
- return 'vAlarm';
- }
-
- function exportvCalendar()
- {
- return parent::_exportvData('VALARM');
- }
-
-}
-
-/**
- * Class representing vEvents.
- *
- * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @since Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vevent extends Horde_iCalendar {
-
- function getType()
- {
- return 'vEvent';
- }
-
- function exportvCalendar()
- {
- // Default values.
- $requiredAttributes = array();
- $requiredAttributes['DTSTAMP'] = time();
- $requiredAttributes['UID'] = $this->_exportDateTime(time())
- . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16)
- . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
-
- $method = !empty($this->_container) ?
- $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
- switch ($method) {
- case 'PUBLISH':
- $requiredAttributes['DTSTART'] = time();
- $requiredAttributes['SUMMARY'] = '';
- break;
-
- case 'REQUEST':
- $requiredAttributes['ATTENDEE'] = '';
- $requiredAttributes['DTSTART'] = time();
- $requiredAttributes['SUMMARY'] = '';
- break;
-
- case 'REPLY':
- $requiredAttributes['ATTENDEE'] = '';
- break;
-
- case 'ADD':
- $requiredAttributes['DTSTART'] = time();
- $requiredAttributes['SEQUENCE'] = 1;
- $requiredAttributes['SUMMARY'] = '';
- break;
-
- case 'CANCEL':
- $requiredAttributes['ATTENDEE'] = '';
- $requiredAttributes['SEQUENCE'] = 1;
- break;
-
- case 'REFRESH':
- $requiredAttributes['ATTENDEE'] = '';
- break;
- }
-
- foreach ($requiredAttributes as $name => $default_value) {
- if (is_a($this->getAttribute($name), 'PEAR_Error')) {
- $this->setAttribute($name, $default_value);
- }
- }
-
- return parent::_exportvData('VEVENT');
- }
-
- /**
- * Update the status of an attendee of an event.
- *
- * @param $email The email address of the attendee.
- * @param $status The participant status to set.
- * @param $fullname The full name of the participant to set.
- */
- function updateAttendee($email, $status, $fullname = '')
- {
- foreach ($this->_attributes as $key => $attribute) {
- if ($attribute['name'] == 'ATTENDEE' &&
- $attribute['value'] == 'mailto:' . $email) {
- $this->_attributes[$key]['params']['PARTSTAT'] = $status;
- if (!empty($fullname)) {
- $this->_attributes[$key]['params']['CN'] = $fullname;
- }
- unset($this->_attributes[$key]['params']['RSVP']);
- return;
- }
- }
- $params = array('PARTSTAT' => $status);
- if (!empty($fullname)) {
- $params['CN'] = $fullname;
- }
- $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params);
- }
-
- /**
- * Return the organizer display name or email.
- *
- * @return string The organizer name to display for this event.
- */
- function organizerName()
- {
- $organizer = $this->getAttribute('ORGANIZER', true);
- if (is_a($organizer, 'PEAR_Error')) {
- return _("An unknown person");
- }
-
- if (isset($organizer[0]['CN'])) {
- return $organizer[0]['CN'];
- }
-
- $organizer = parse_url($this->getAttribute('ORGANIZER'));
-
- return $organizer['path'];
- }
-
- /**
- * Update this event with details from another event.
- *
- * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details.
- */
- function updateFromvEvent($vevent)
- {
- $newAttributes = $vevent->getAllAttributes();
- foreach ($newAttributes as $newAttribute) {
- $currentValue = $this->getAttribute($newAttribute['name']);
- if (is_a($currentValue, 'PEAR_error')) {
- // Already exists so just add it.
- $this->setAttribute($newAttribute['name'],
- $newAttribute['value'],
- $newAttribute['params']);
- } else {
- // Already exists so locate and modify.
- $found = false;
-
- // Try matching the attribte name and value incase
- // only the params changed (eg attendee updating
- // status).
- foreach ($this->_attributes as $id => $attr) {
- if ($attr['name'] == $newAttribute['name'] &&
- $attr['value'] == $newAttribute['value']) {
- // merge the params
- foreach ($newAttribute['params'] as $param_id => $param_name) {
- $this->_attributes[$id]['params'][$param_id] = $param_name;
- }
- $found = true;
- break;
- }
- }
- if (!$found) {
- // Else match the first attribute with the same
- // name (eg changing start time).
- foreach ($this->_attributes as $id => $attr) {
- if ($attr['name'] == $newAttribute['name']) {
- $this->_attributes[$id]['value'] = $newAttribute['value'];
- // Merge the params.
- foreach ($newAttribute['params'] as $param_id => $param_name) {
- $this->_attributes[$id]['params'][$param_id] = $param_name;
- }
- break;
- }
- }
- }
- }
- }
- }
-
- /**
- * Update just the attendess of event with details from another
- * event.
- *
- * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details
- */
- function updateAttendeesFromvEvent($vevent)
- {
- $newAttributes = $vevent->getAllAttributes();
- foreach ($newAttributes as $newAttribute) {
- if ($newAttribute['name'] != 'ATTENDEE') {
- continue;
- }
- $currentValue = $this->getAttribute($newAttribute['name']);
- if (is_a($currentValue, 'PEAR_error')) {
- // Already exists so just add it.
- $this->setAttribute($newAttribute['name'],
- $newAttribute['value'],
- $newAttribute['params']);
- } else {
- // Already exists so locate and modify.
- $found = false;
- // Try matching the attribte name and value incase
- // only the params changed (eg attendee updating
- // status).
- foreach ($this->_attributes as $id => $attr) {
- if ($attr['name'] == $newAttribute['name'] &&
- $attr['value'] == $newAttribute['value']) {
- // Merge the params.
- foreach ($newAttribute['params'] as $param_id => $param_name) {
- $this->_attributes[$id]['params'][$param_id] = $param_name;
- }
- $found = true;
- break;
- }
- }
-
- if (!$found) {
- // Else match the first attribute with the same
- // name (eg changing start time).
- foreach ($this->_attributes as $id => $attr) {
- if ($attr['name'] == $newAttribute['name']) {
- $this->_attributes[$id]['value'] = $newAttribute['value'];
- // Merge the params.
- foreach ($newAttribute['params'] as $param_id => $param_name) {
- $this->_attributes[$id]['params'][$param_id] = $param_name;
- }
- break;
- }
- }
- }
- }
- }
- }
-
-}
-
-/**
- * Class representing vFreebusy components.
- *
- * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @todo Don't use timestamps
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @since Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vfreebusy extends Horde_iCalendar {
-
- var $_busyPeriods = array();
- var $_extraParams = array();
-
- /**
- * Returns the type of this calendar component.
- *
- * @return string The type of this component.
- */
- function getType()
- {
- return 'vFreebusy';
- }
-
- /**
- * Parses a string containing vFreebusy data.
- *
- * @param string $data The data to parse.
- */
- function parsevCalendar($data, $type = null, $charset = null)
- {
- parent::parsevCalendar($data, 'VFREEBUSY', $charset);
-
- // Do something with all the busy periods.
- foreach ($this->_attributes as $key => $attribute) {
- if ($attribute['name'] != 'FREEBUSY') {
- continue;
- }
- foreach ($attribute['values'] as $value) {
- $params = isset($attribute['params'])
- ? $attribute['params']
- : array();
- if (isset($value['duration'])) {
- $this->addBusyPeriod('BUSY', $value['start'], null,
- $value['duration'], $params);
- } else {
- $this->addBusyPeriod('BUSY', $value['start'],
- $value['end'], null, $params);
- }
- }
- unset($this->_attributes[$key]);
- }
- }
-
- /**
- * Returns the component exported as string.
- *
- * @return string The exported vFreeBusy information according to the
- * iCalender format specification.
- */
- function exportvCalendar()
- {
- foreach ($this->_busyPeriods as $start => $end) {
- $periods = array(array('start' => $start, 'end' => $end));
- $this->setAttribute('FREEBUSY', $periods,
- isset($this->_extraParams[$start])
- ? $this->_extraParams[$start] : array());
- }
-
- $res = parent::_exportvData('VFREEBUSY');
-
- foreach ($this->_attributes as $key => $attribute) {
- if ($attribute['name'] == 'FREEBUSY') {
- unset($this->_attributes[$key]);
- }
- }
-
- return $res;
- }
-
- /**
- * Returns a display name for this object.
- *
- * @return string A clear text name for displaying this object.
- */
- function getName()
- {
- $name = '';
- $method = !empty($this->_container) ?
- $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
- if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
- $attr = 'ORGANIZER';
- } elseif ($method == 'REPLY') {
- $attr = 'ATTENDEE';
- }
-
- $name = $this->getAttribute($attr, true);
- if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) {
- return $name[0]['CN'];
- }
-
- $name = $this->getAttribute($attr);
- if (is_a($name, 'PEAR_Error')) {
- return '';
- } else {
- $name = parse_url($name);
- return $name['path'];
- }
- }
-
- /**
- * Returns the email address for this object.
- *
- * @return string The email address of this object's owner.
- */
- function getEmail()
- {
- $name = '';
- $method = !empty($this->_container)
- ? $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
- if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
- $attr = 'ORGANIZER';
- } elseif ($method == 'REPLY') {
- $attr = 'ATTENDEE';
- }
-
- $name = $this->getAttribute($attr);
- if (is_a($name, 'PEAR_Error')) {
- return '';
- } else {
- $name = parse_url($name);
- return $name['path'];
- }
- }
-
- /**
- * Returns the busy periods.
- *
- * @return array All busy periods.
- */
- function getBusyPeriods()
- {
- return $this->_busyPeriods;
- }
-
- /**
- * Returns any additional freebusy parameters.
- *
- * @return array Additional parameters of the freebusy periods.
- */
- function getExtraParams()
- {
- return $this->_extraParams;
- }
-
- /**
- * Returns all the free periods of time in a given period.
- *
- * @param integer $startStamp The start timestamp.
- * @param integer $endStamp The end timestamp.
- *
- * @return array A hash with free time periods, the start times as the
- * keys and the end times as the values.
- */
- function getFreePeriods($startStamp, $endStamp)
- {
- $this->simplify();
- $periods = array();
-
- // Check that we have data for some part of this period.
- if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
- return $periods;
- }
-
- // Locate the first time in the requested period we have data for.
- $nextstart = max($startStamp, $this->getStart());
-
- // Check each busy period and add free periods in between.
- foreach ($this->_busyPeriods as $start => $end) {
- if ($start <= $endStamp && $end >= $nextstart) {
- if ($nextstart <= $start) {
- $periods[$nextstart] = min($start, $endStamp);
- }
- $nextstart = min($end, $endStamp);
- }
- }
-
- // If we didn't read the end of the requested period but still have
- // data then mark as free to the end of the period or available data.
- if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
- $periods[$nextstart] = min($this->getEnd(), $endStamp);
- }
-
- return $periods;
- }
-
- /**
- * Adds a busy period to the info.
- *
- * This function may throw away data in case you add a period with a start
- * date that already exists. The longer of the two periods will be chosen
- * (and all information associated with the shorter one will be removed).
- *
- * @param string $type The type of the period. Either 'FREE' or
- * 'BUSY'; only 'BUSY' supported at the moment.
- * @param integer $start The start timestamp of the period.
- * @param integer $end The end timestamp of the period.
- * @param integer $duration The duration of the period. If specified, the
- * $end parameter will be ignored.
- * @param array $extra Additional parameters for this busy period.
- */
- function addBusyPeriod($type, $start, $end = null, $duration = null,
- $extra = array())
- {
- if ($type == 'FREE') {
- // Make sure this period is not marked as busy.
- return false;
- }
-
- // Calculate the end time if duration was specified.
- $tempEnd = is_null($duration) ? $end : $start + $duration;
-
- // Make sure the period length is always positive.
- $end = max($start, $tempEnd);
- $start = min($start, $tempEnd);
-
- if (isset($this->_busyPeriods[$start])) {
- // Already a period starting at this time. Change the current
- // period only if the new one is longer. This might be a problem
- // if the callee assumes that there is no simplification going
- // on. But since the periods are stored using the start time of
- // the busy periods we have to throw away data here.
- if ($end > $this->_busyPeriods[$start]) {
- $this->_busyPeriods[$start] = $end;
- $this->_extraParams[$start] = $extra;
- }
- } else {
- // Add a new busy period.
- $this->_busyPeriods[$start] = $end;
- $this->_extraParams[$start] = $extra;
- }
-
- return true;
- }
-
- /**
- * Returns the timestamp of the start of the time period this free busy
- * information covers.
- *
- * @return integer A timestamp.
- */
- function getStart()
- {
- if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) {
- return $this->getAttribute('DTSTART');
- } elseif (count($this->_busyPeriods)) {
- return min(array_keys($this->_busyPeriods));
- } else {
- return false;
- }
- }
-
- /**
- * Returns the timestamp of the end of the time period this free busy
- * information covers.
- *
- * @return integer A timestamp.
- */
- function getEnd()
- {
- if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) {
- return $this->getAttribute('DTEND');
- } elseif (count($this->_busyPeriods)) {
- return max(array_values($this->_busyPeriods));
- } else {
- return false;
- }
- }
-
- /**
- * Merges the busy periods of another Horde_iCalendar_vfreebusy object
- * into this one.
- *
- * This might lead to simplification no matter what you specify for the
- * "simplify" flag since periods with the same start date will lead to the
- * shorter period being removed (see addBusyPeriod).
- *
- * @param Horde_iCalendar_vfreebusy $freebusy A freebusy object.
- * @param boolean $simplify If true, simplify() will
- * called after the merge.
- */
- function merge($freebusy, $simplify = true)
- {
- if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) {
- return false;
- }
-
- $extra = $freebusy->getExtraParams();
- foreach ($freebusy->getBusyPeriods() as $start => $end) {
- // This might simplify the busy periods without taking the
- // "simplify" flag into account.
- $this->addBusyPeriod('BUSY', $start, $end, null,
- isset($extra[$start])
- ? $extra[$start] : array());
- }
-
- $thisattr = $this->getAttribute('DTSTART');
- $thatattr = $freebusy->getAttribute('DTSTART');
- if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
- $this->setAttribute('DTSTART', $thatattr, array(), false);
- } elseif (!is_a($thatattr, 'PEAR_Error')) {
- if ($thatattr < $thisattr) {
- $this->setAttribute('DTSTART', $thatattr, array(), false);
- }
- }
-
- $thisattr = $this->getAttribute('DTEND');
- $thatattr = $freebusy->getAttribute('DTEND');
- if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
- $this->setAttribute('DTEND', $thatattr, array(), false);
- } elseif (!is_a($thatattr, 'PEAR_Error')) {
- if ($thatattr > $thisattr) {
- $this->setAttribute('DTEND', $thatattr, array(), false);
- }
- }
-
- if ($simplify) {
- $this->simplify();
- }
-
- return true;
- }
-
- /**
- * Removes all overlaps and simplifies the busy periods array as much as
- * possible.
- */
- function simplify()
- {
- $clean = false;
- $busy = array($this->_busyPeriods, $this->_extraParams);
- while (!$clean) {
- $result = $this->_simplify($busy[0], $busy[1]);
- $clean = $result === $busy;
- $busy = $result;
- }
-
- ksort($result[1], SORT_NUMERIC);
- $this->_extraParams = $result[1];
-
- ksort($result[0], SORT_NUMERIC);
- $this->_busyPeriods = $result[0];
- }
-
- function _simplify($busyPeriods, $extraParams = array())
- {
- $checked = array();
- $checkedExtra = array();
- $checkedEmpty = true;
-
- foreach ($busyPeriods as $start => $end) {
- if ($checkedEmpty) {
- $checked[$start] = $end;
- $checkedExtra[$start] = isset($extraParams[$start])
- ? $extraParams[$start] : array();
- $checkedEmpty = false;
- } else {
- $added = false;
- foreach ($checked as $testStart => $testEnd) {
- // Replace old period if the new period lies around the
- // old period.
- if ($start <= $testStart && $end >= $testEnd) {
- // Remove old period entry.
- unset($checked[$testStart]);
- unset($checkedExtra[$testStart]);
- // Add replacing entry.
- $checked[$start] = $end;
- $checkedExtra[$start] = isset($extraParams[$start])
- ? $extraParams[$start] : array();
- $added = true;
- } elseif ($start >= $testStart && $end <= $testEnd) {
- // The new period lies fully within the old
- // period. Just forget about it.
- $added = true;
- } elseif (($end <= $testEnd && $end >= $testStart) ||
- ($start >= $testStart && $start <= $testEnd)) {
- // Now we are in trouble: Overlapping time periods. If
- // we allow for additional parameters we cannot simply
- // choose one of the two parameter sets. It's better
- // to leave two separated time periods.
- $extra = isset($extraParams[$start])
- ? $extraParams[$start] : array();
- $testExtra = isset($checkedExtra[$testStart])
- ? $checkedExtra[$testStart] : array();
- // Remove old period entry.
- unset($checked[$testStart]);
- unset($checkedExtra[$testStart]);
- // We have two periods overlapping. Are their
- // additional parameters the same or different?
- $newStart = min($start, $testStart);
- $newEnd = max($end, $testEnd);
- if ($extra === $testExtra) {
- // Both periods have the same information. So we
- // can just merge.
- $checked[$newStart] = $newEnd;
- $checkedExtra[$newStart] = $extra;
- } else {
- // Extra parameters are different. Create one
- // period at the beginning with the params of the
- // first period and create a trailing period with
- // the params of the second period. The break
- // point will be the end of the first period.
- $break = min($end, $testEnd);
- $checked[$newStart] = $break;
- $checkedExtra[$newStart] =
- isset($extraParams[$newStart])
- ? $extraParams[$newStart] : array();
- $checked[$break] = $newEnd;
- $highStart = max($start, $testStart);
- $checkedExtra[$break] =
- isset($extraParams[$highStart])
- ? $extraParams[$highStart] : array();
-
- // Ensure we also have the extra data in the
- // extraParams.
- $extraParams[$break] =
- isset($extraParams[$highStart])
- ? $extraParams[$highStart] : array();
- }
- $added = true;
- }
-
- if ($added) {
- break;
- }
- }
-
- if (!$added) {
- $checked[$start] = $end;
- $checkedExtra[$start] = isset($extraParams[$start])
- ? $extraParams[$start] : array();
- }
- }
- }
-
- return array($checked, $checkedExtra);
- }
-
-}
-
-/**
- * Class representing vJournals.
- *
- * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @since Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vjournal extends Horde_iCalendar {
-
- function getType()
- {
- return 'vJournal';
- }
-
- function exportvCalendar()
- {
- return parent::_exportvData('VJOURNAL');
- }
-
-}
-
-
-
-
-/**
- * Class representing vNotes.
- *
- * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @author Karsten Fourmont <fourmont at gmx.de>
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vnote extends Horde_iCalendar {
-
- function Horde_iCalendar_vnote($version = '1.1')
- {
- return parent::Horde_iCalendar($version);
- }
-
- function getType()
- {
- return 'vNote';
- }
-
- /**
- * Unlike vevent and vtodo, a vnote is normally not enclosed in an
- * iCalendar container. (BEGIN..END)
- */
- function exportvCalendar()
- {
- $requiredAttributes['BODY'] = '';
- $requiredAttributes['VERSION'] = '1.1';
-
- foreach ($requiredAttributes as $name => $default_value) {
- if (is_a($this->getattribute($name), 'PEAR_Error')) {
- $this->setAttribute($name, $default_value);
- }
- }
-
- return $this->_exportvData('VNOTE');
- }
-
-}
-
-/**
- * Class representing vTimezones.
- *
- * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @since Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vtimezone extends Horde_iCalendar {
-
- function getType()
- {
- return 'vTimeZone';
- }
-
- function exportvCalendar()
- {
- return parent::_exportvData('VTIMEZONE');
- }
-
- /**
- * Parse child components of the vTimezone component. Returns an
- * array with the exact time of the time change as well as the
- * 'from' and 'to' offsets around the change. Time is arbitrarily
- * based on UTC for comparison.
- */
- function parseChild(&$child, $year)
- {
- // Make sure 'time' key is first for sort().
- $result['time'] = 0;
-
- $t = $child->getAttribute('TZOFFSETFROM');
- if (is_a($t, 'PEAR_Error')) {
- return false;
- }
- $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
-
- $t = $child->getAttribute('TZOFFSETTO');
- if (is_a($t, 'PEAR_Error')) {
- return false;
- }
- $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
-
- $switch_time = $child->getAttribute('DTSTART');
- if (is_a($switch_time, 'PEAR_Error')) {
- return false;
- }
-
- $rrules = $child->getAttribute('RRULE');
- if (is_a($rrules, 'PEAR_Error')) {
- if (!is_int($switch_time)) {
- return false;
- }
- // Convert this timestamp from local time to UTC for
- // comparison (All dates are compared as if they are UTC).
- $t = getdate($switch_time);
- $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'],
- $t['mon'], $t['mday'], $t['year']);
- return $result;
- }
-
- $rrules = explode(';', $rrules);
- foreach ($rrules as $rrule) {
- $t = explode('=', $rrule);
- switch ($t[0]) {
- case 'FREQ':
- if ($t[1] != 'YEARLY') {
- return false;
- }
- break;
-
- case 'INTERVAL':
- if ($t[1] != '1') {
- return false;
- }
- break;
-
- case 'BYMONTH':
- $month = intval($t[1]);
- break;
-
- case 'BYDAY':
- $len = strspn($t[1], '1234567890-+');
- if ($len == 0) {
- return false;
- }
- $weekday = substr($t[1], $len);
- $weekdays = array(
- 'SU' => 0,
- 'MO' => 1,
- 'TU' => 2,
- 'WE' => 3,
- 'TH' => 4,
- 'FR' => 5,
- 'SA' => 6
- );
- $weekday = $weekdays[$weekday];
- $which = intval(substr($t[1], 0, $len));
- break;
-
- case 'UNTIL':
- if (intval($year) > intval(substr($t[1], 0, 4))) {
- return false;
- }
- break;
- }
- }
-
- if (empty($month) || !isset($weekday)) {
- return false;
- }
-
- if (is_int($switch_time)) {
- // Was stored as localtime.
- $switch_time = strftime('%H:%M:%S', $switch_time);
- $switch_time = explode(':', $switch_time);
- } else {
- $switch_time = explode('T', $switch_time);
- if (count($switch_time) != 2) {
- return false;
- }
- $switch_time[0] = substr($switch_time[1], 0, 2);
- $switch_time[2] = substr($switch_time[1], 4, 2);
- $switch_time[1] = substr($switch_time[1], 2, 2);
- }
-
- // Get the timestamp for the first day of $month.
- $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2],
- $month, 1, $year);
- // Get the day of the week for the first day of $month.
- $first_of_month_weekday = intval(gmstrftime('%w', $when));
-
- // Go to the first $weekday before first day of $month.
- if ($weekday >= $first_of_month_weekday) {
- $weekday -= 7;
- }
- $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24;
-
- // If going backwards go to the first $weekday after last day
- // of $month.
- if ($which < 0) {
- do {
- $when += 60*60*24*7;
- } while (intval(gmstrftime('%m', $when)) == $month);
- }
-
- // Calculate $weekday number $which.
- $when += $which * 60 * 60 * 24 * 7;
-
- $result['time'] = $when;
-
- return $result;
- }
-
-}
-
-/**
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_standard extends Horde_iCalendar {
-
- function getType()
- {
- return 'standard';
- }
-
- function parsevCalendar($data)
- {
- parent::parsevCalendar($data, 'STANDARD');
- }
-
- function exportvCalendar()
- {
- return parent::_exportvData('STANDARD');
- }
-
-}
-
-/**
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_daylight extends Horde_iCalendar {
-
- function getType()
- {
- return 'daylight';
- }
-
- function parsevCalendar($data)
- {
- parent::parsevCalendar($data, 'DAYLIGHT');
- }
-
- function exportvCalendar()
- {
- return parent::_exportvData('DAYLIGHT');
- }
-
-}
-
-/**
- * Class representing vTodos.
- *
- * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author Mike Cochrane <mike at graftonhall.co.nz>
- * @since Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vtodo extends Horde_iCalendar {
-
- function getType()
- {
- return 'vTodo';
- }
-
- function exportvCalendar()
- {
- return parent::_exportvData('VTODO');
- }
-
- /**
- * Convert this todo to an array of attributes.
- *
- * @return array Array containing the details of the todo in a hash
- * as used by Horde applications.
- */
- function toArray()
- {
- $todo = array();
-
- $name = $this->getAttribute('SUMMARY');
- if (!is_array($name) && !is_a($name, 'PEAR_Error')) {
- $todo['name'] = $name;
- }
- $desc = $this->getAttribute('DESCRIPTION');
- if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) {
- $todo['desc'] = $desc;
- }
-
- $priority = $this->getAttribute('PRIORITY');
- if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) {
- $todo['priority'] = $priority;
- }
-
- $due = $this->getAttribute('DTSTAMP');
- if (!is_array($due) && !is_a($due, 'PEAR_Error')) {
- $todo['due'] = $due;
- }
-
- return $todo;
- }
-
- /**
- * Set the attributes for this todo item from an array.
- *
- * @param array $todo Array containing the details of the todo in
- * the same format that toArray() exports.
- */
- function fromArray($todo)
- {
- if (isset($todo['name'])) {
- $this->setAttribute('SUMMARY', $todo['name']);
- }
- if (isset($todo['desc'])) {
- $this->setAttribute('DESCRIPTION', $todo['desc']);
- }
-
- if (isset($todo['priority'])) {
- $this->setAttribute('PRIORITY', $todo['priority']);
- }
-
- if (isset($todo['due'])) {
- $this->setAttribute('DTSTAMP', $todo['due']);
- }
- }
-
-}
diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php
deleted file mode 100644
index 66557fd..0000000
--- a/plugins/calendar/lib/calendar_ical.php
+++ /dev/null
@@ -1,569 +0,0 @@
-<?php
-
-/**
- * iCalendar functions for the Calendar plugin
- *
- * @version @package_version@
- * @author Lazlo Westerhof <hello at lazlo.me>
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- * @author Bogomil "Bogo" Shopov <shopov at kolabsys.com>
- *
- * Copyright (C) 2010, Lazlo Westerhof <hello at lazlo.me>
- * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program 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 Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-
-/**
- * Class to parse and build vCalendar (iCalendar) files
- *
- * Uses the Horde:iCalendar class for parsing. To install:
- * > pear channel-discover pear.horde.org
- * > pear install horde/Horde_Icalendar
- *
- */
-class calendar_ical
-{
- const EOL = "\r\n";
-
- private $rc;
- private $cal;
-
- public $method;
- public $events = array();
-
- function __construct($cal)
- {
- $this->cal = $cal;
- $this->rc = $cal->rc;
- }
-
- /**
- * Import events from iCalendar format
- *
- * @param string vCalendar input
- * @param string Input charset (from envelope)
- * @return array List of events extracted from the input
- */
- public function import($vcal, $charset = RCMAIL_CHARSET)
- {
- $parser = $this->get_parser();
- $parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
- $this->method = $parser->getAttributeDefault('METHOD', '');
- $this->events = $seen = array();
- if ($data = $parser->getComponents()) {
- foreach ($data as $comp) {
- if ($comp->getType() == 'vEvent') {
- $event = $this->_to_rcube_format($comp);
- if (!$seen[$event['uid']]++)
- $this->events[] = $event;
- }
- }
- }
-
- return $this->events;
- }
-
- /**
- * Read iCalendar events from a file
- *
- * @param string File path to read from
- * @return array List of events extracted from the file
- */
- public function import_from_file($filepath)
- {
- $this->events = $seen = array();
- $fp = fopen($filepath, 'r');
-
- // check file content first
- $begin = fread($fp, 1024);
- if (!preg_match('/BEGIN:VCALENDAR/i', $begin))
- return $this->events;
-
- $parser = $this->get_parser();
- $buffer = '';
-
- fseek($fp, 0);
- while (($line = fgets($fp, 2048)) !== false) {
- $buffer .= $line;
- if (preg_match('/END:VEVENT/i', $line)) {
- if (preg_match('/BEGIN:VCALENDAR/i', $buffer))
- $buffer .= self::EOL ."END:VCALENDAR";
- $parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false);
- $buffer = '';
- }
- }
- fclose($fp);
-
- if ($data = $parser->getComponents()) {
- foreach ($data as $comp) {
- if ($comp->getType() == 'vEvent') {
- $event = $this->_to_rcube_format($comp);
- if (!$seen[$event['uid']]++)
- $this->events[] = $event;
- }
- }
- }
-
- return $this->events;
- }
-
- /**
- * Load iCal parser from the Horde lib
- */
- public function get_parser()
- {
- if (!class_exists('Horde_iCalendar'))
- require_once($this->cal->home . '/lib/Horde_iCalendar.php');
-
- // set target charset for parsed events
- $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
-
- return new Horde_iCalendar;
- }
-
- /**
- * Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
- */
- private function _to_rcube_format($ve)
- {
- $event = array(
- 'uid' => $ve->getAttributeDefault('UID'),
- 'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
- 'title' => $ve->getAttributeDefault('SUMMARY'),
- 'start' => $this->_date2time($ve->getAttribute('DTSTART')),
- 'end' => $this->_date2time($ve->getAttribute('DTEND')),
- // set defaults
- 'free_busy' => 'busy',
- 'priority' => 0,
- 'attendees' => array(),
- );
-
- // check for all-day dates
- if (is_array($ve->getAttribute('DTSTART')))
- $event['allday'] = true;
-
- if ($event['allday'])
- $event['end']->sub(new DateInterval('PT23H'));
-
- // assign current timezone to event start/end
- if (is_a($event['start'], 'DateTime'))
- $event['start']->setTimezone($this->cal->timezone);
- else
- unset($event['start']);
-
- if (is_a($event['end'], 'DateTime'))
- $event['end']->setTimezone($this->cal->timezone);
- else
- unset($event['end']);
-
- // map other attributes to internal fields
- $_attendees = array();
- foreach ($ve->getAllAttributes() as $attr) {
- switch ($attr['name']) {
- case 'ORGANIZER':
- $organizer = array(
- 'name' => $attr['params']['CN'],
- 'email' => preg_replace('/^mailto:/i', '', $attr['value']),
- 'role' => 'ORGANIZER',
- 'status' => 'ACCEPTED',
- );
- if (isset($_attendees[$organizer['email']])) {
- $i = $_attendees[$organizer['email']];
- $event['attendees'][$i]['role'] = $organizer['role'];
- }
- break;
-
- case 'ATTENDEE':
- $attendee = array(
- 'name' => $attr['params']['CN'],
- 'email' => preg_replace('/^mailto:/i', '', $attr['value']),
- 'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
- 'status' => $attr['params']['PARTSTAT'],
- 'rsvp' => $attr['params']['RSVP'] == 'TRUE',
- );
- if ($organizer && $organizer['email'] == $attendee['email'])
- $attendee['role'] = 'ORGANIZER';
-
- $event['attendees'][] = $attendee;
- $_attendees[$attendee['email']] = count($event['attendees']) - 1;
- break;
-
- case 'TRANSP':
- $event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
- break;
-
- case 'STATUS':
- if ($attr['value'] == 'TENTATIVE')
- $event['free_busy'] = 'tentative';
- else if ($attr['value'] == 'CANCELLED')
- $event['cancelled'] = true;
- break;
-
- case 'PRIORITY':
- if (is_numeric($attr['value'])) {
- $event['priority'] = $attr['value'];
- }
- break;
-
- case 'RRULE':
- // parse recurrence rule attributes
- foreach (explode(';', $attr['value']) as $par) {
- list($k, $v) = explode('=', $par);
- $params[$k] = $v;
- }
- if ($params['UNTIL'])
- $params['UNTIL'] = date_create($params['UNTIL']);
- if (!$params['INTERVAL'])
- $params['INTERVAL'] = 1;
-
- $event['recurrence'] = $params;
- break;
-
- case 'EXDATE':
- break;
-
- case 'RECURRENCE-ID':
- $event['recurrence_id'] = $this->_date2time($attr['value']);
- break;
-
- case 'SEQUENCE':
- $event['sequence'] = intval($attr['value']);
- break;
-
- case 'DESCRIPTION':
- case 'LOCATION':
- case 'URL':
- $event[strtolower($attr['name'])] = $attr['value'];
- break;
-
- case 'CLASS':
- case 'X-CALENDARSERVER-ACCESS':
- $sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
- $event['sensitivity'] = $sensitivity_map[$attr['value']];
- break;
-
- case 'X-MICROSOFT-CDO-BUSYSTATUS':
- if ($attr['value'] == 'OOF')
- $event['free_busy'] == 'outofoffice';
- else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
- $event['free_busy'] = strtolower($attr['value']);
- break;
-
- case 'ATTACH':
- // decode inline attachment
- if (strtoupper($attr['params']['VALUE']) == 'BINARY' && !empty($attr['value'])) {
- $data = !strcasecmp($attr['params']['ENCODING'], 'BASE64') ? base64_decode($attr['value']) : $attr['value'];
- $mimetype = $attr['params']['FMTTYPE'] ? $attr['params']['FMTTYPE'] : rcube_mime::file_content_type($data, $attr['params']['X-LABEL'], 'application/octet-stream', true);
- $extensions = rcube_mime::get_mime_extensions($mimetype);
- $filename = $attr['params']['X-LABEL'] ? $attr['params']['X-LABEL'] : 'attachment' . count($event['attachments']) . '.' . $extensions[0];
- $event['attachments'][] = array(
- 'mimetype' => $mimetype,
- 'name' => $filename,
- 'data' => $data,
- 'size' => strlen($data),
- );
- }
- else if (!empty($attr['value']) && preg_match('!^[hftps]+://!', $attr['value'])) {
- // TODO: add support for displaying/managing link attachments in UI
- $event['links'][] = $attr['value'];
- }
- break;
-
- default:
- if (substr($attr['name'], 0, 2) == 'X-')
- $event['x-custom'][] = array($attr['name'], $attr['value']);
- }
- }
-
- // find alarms
- if ($valarm = $ve->findComponent('valarm')) {
- $action = 'DISPLAY';
- $trigger = null;
-
- foreach ($valarm->getAllAttributes() as $attr) {
- switch ($attr['name']) {
- case 'TRIGGER':
- if ($attr['params']['VALUE'] == 'DATE-TIME') {
- $trigger = '@' . $attr['value'];
- }
- else {
- $trigger = $attr['value'];
- $offset = abs($trigger);
- $unit = 'S';
- if ($offset % 86400 == 0) {
- $unit = 'D';
- $trigger = intval($trigger / 86400);
- }
- else if ($offset % 3600 == 0) {
- $unit = 'H';
- $trigger = intval($trigger / 3600);
- }
- else if ($offset % 60 == 0) {
- $unit = 'M';
- $trigger = intval($trigger / 60);
- }
- }
- break;
-
- case 'ACTION':
- $action = $attr['value'];
- break;
- }
- }
- if ($trigger)
- $event['alarms'] = $trigger . $unit . ':' . $action;
- }
-
- // add organizer to attendees list if not already present
- if ($organizer && !isset($_attendees[$organizer['email']]))
- array_unshift($event['attendees'], $organizer);
-
- // make sure the event has an UID
- if (!$event['uid'])
- $event['uid'] = $this->cal->generate_uid();
-
- return $event;
- }
-
- /**
- * Helper method to correctly interpret an all-day date value
- */
- private function _date2time($prop)
- {
- // create timestamp at 12:00 in user's timezone
- if (is_array($prop))
- return date_create(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->cal->timezone);
- else if (is_numeric($prop))
- return date_create('@'.$prop);
-
- return $prop;
- }
-
-
- /**
- * Free resources by clearing member vars
- */
- public function reset()
- {
- $this->method = '';
- $this->events = array();
- }
-
- /**
- * Export events to iCalendar format
- *
- * @param array Events as array
- * @param string VCalendar method to advertise
- * @param boolean Directly send data to stdout instead of returning
- * @param callable Callback function to fetch attachment contents, false if no attachment export
- * @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
- */
- public function export($events, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
- {
- $memory_limit = parse_bytes(ini_get('memory_limit'));
-
- if (!$recurrence_id) {
- $ical = "BEGIN:VCALENDAR" . self::EOL;
- $ical .= "VERSION:2.0" . self::EOL;
- $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
- $ical .= "CALSCALE:GREGORIAN" . self::EOL;
-
- if ($method)
- $ical .= "METHOD:" . strtoupper($method) . self::EOL;
-
- if ($write) {
- echo $ical;
- $ical = '';
- }
- }
-
- foreach ($events as $event) {
- $vevent = "BEGIN:VEVENT" . self::EOL;
- $vevent .= "UID:" . self::escape($event['uid']) . self::EOL;
- $vevent .= $this->format_datetime("DTSTAMP", $event['changed'] ?: new DateTime(), false, true) . self::EOL;
- if ($event['sequence'])
- $vevent .= "SEQUENCE:" . intval($event['sequence']) . self::EOL;
- if ($recurrence_id)
- $vevent .= $recurrence_id . self::EOL;
-
- // correctly set all-day dates
- if ($event['allday']) {
- $event['end'] = clone $event['end'];
- $event['end']->add(new DateInterval('P1D')); // ends the next day
- $vevent .= $this->format_datetime("DTSTART", $event['start'], true) . self::EOL;
- $vevent .= $this->format_datetime("DTEND", $event['end'], true) . self::EOL;
- }
- else {
- $vevent .= $this->format_datetime("DTSTART", $event['start'], false) . self::EOL;
- $vevent .= $this->format_datetime("DTEND", $event['end'], false) . self::EOL;
- }
- $vevent .= "SUMMARY:" . self::escape($event['title']) . self::EOL;
- $vevent .= "DESCRIPTION:" . self::escape($event['description']) . self::EOL;
-
- if (!empty($event['attendees'])){
- $vevent .= $this->_get_attendees($event['attendees']);
- }
-
- if (!empty($event['location'])) {
- $vevent .= "LOCATION:" . self::escape($event['location']) . self::EOL;
- }
- if (!empty($event['url'])) {
- $vevent .= "URL:" . self::escape($event['url']) . self::EOL;
- }
- if ($event['recurrence'] && !$recurrence_id) {
- $vevent .= "RRULE:" . libcalendaring::to_rrule($event['recurrence'], self::EOL) . self::EOL;
- }
- if(!empty($event['categories'])) {
- $vevent .= "CATEGORIES:" . self::escape(strtoupper($event['categories'])) . self::EOL;
- }
- if ($event['sensitivity'] > 0) {
- $vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL;
- }
- if ($event['alarms']) {
- list($trigger, $action) = explode(':', $event['alarms']);
- $val = libcalendaring::parse_alaram_value($trigger);
-
- $vevent .= "BEGIN:VALARM\n";
- if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
- else $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
- if ($action) $vevent .= "ACTION:" . self::escape(strtoupper($action)) . self::EOL;
- $vevent .= "END:VALARM\n";
- }
-
- $vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
-
- if ($event['priority']) {
- $vevent .= "PRIORITY:" . $event['priority'] . self::EOL;
- }
-
- if ($event['cancelled'])
- $vevent .= "STATUS:CANCELLED" . self::EOL;
- else if ($event['free_busy'] == 'tentative')
- $vevent .= "STATUS:TENTATIVE" . self::EOL;
-
- foreach ((array)$event['x-custom'] as $prop)
- $vevent .= $prop[0] . ':' . self::escape($prop[1]) . self::EOL;
-
- // export attachments using the given callback function
- if (is_callable($get_attachment) && !empty($event['attachments'])) {
- foreach ((array)$event['attachments'] as $attach) {
- // check available memory and skip attachment export if we can't buffer it
- if ($memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
- && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
- continue;
- }
- // TODO: let the callback print the data directly to stdout (with b64 encoding)
- if ($data = call_user_func($get_attachment, $attach['id'], $event)) {
- $vevent .= sprintf('ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=%s;X-LABEL=%s:',
- self::escape($attach['mimetype']), self::escape($attach['name']));
- $vevent .= base64_encode($data) . self::EOL;
- }
- unset($data); // attempt to free memory
- }
- }
-
- $vevent .= "END:VEVENT" . self::EOL;
-
- // append recurrence exceptions
- if ($event['recurrence']['EXCEPTIONS'] && !$recurrence_id) {
- foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
- $exdate = clone $event['start'];
- $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
- $vevent .= $this->export(array($ex), null, false, $get_attachment,
- $this->format_datetime('RECURRENCE-ID', $exdate, $event['allday']));
- }
- }
-
- if ($write)
- echo rcube_vcard::rfc2425_fold($vevent);
- else
- $ical .= $vevent;
- }
-
- if (!$recurrence_id) {
- $ical .= "END:VCALENDAR" . self::EOL;
-
- if ($write) {
- echo $ical;
- return true;
- }
- }
-
- // fold lines to 75 chars
- return rcube_vcard::rfc2425_fold($ical);
- }
-
- private function format_datetime($attr, $dt, $dateonly = false, $utc = false)
- {
- if (is_numeric($dt))
- $dt = new DateTime('@'.$dt);
-
- if ($utc)
- $dt->setTimezone(new DateTimeZone('UTC'));
-
- if ($dateonly) {
- return $attr . ';VALUE=DATE:' . $dt->format('Ymd');
- }
- else {
- // <ATTR>;TZID=Europe/Zurich:20120706T210000
- $tz = $dt->getTimezone();
- $tzname = $tz ? $tz->getName() : null;
- $tzid = $tzname && $tzname != 'UTC' && $tzname != '+00:00' ? ';TZID=' . self::escape($tzname) : '';
- return $attr . $tzid . ':' . $dt->format('Ymd\THis' . ($tzid ? '' : '\Z'));
- }
- }
-
- /**
- * Escape values according to RFC 2445 4.3.11
- */
- private function escape($str)
- {
- return strtr($str, array('\\' => '\\\\', "\n" => '\n', ';' => '\;', ',' => '\,'));
- }
-
- /**
- * Construct the orginizer of the event.
- * @param Array Attendees and roles
- *
- */
- private function _get_attendees($ats)
- {
- $organizer = "";
- $attendees = "";
- foreach ($ats as $at) {
- if ($at['role'] == "ORGANIZER") {
- if ($at['email']) {
- $organizer .= "ORGANIZER;";
- if (!empty($at['name']))
- $organizer .= 'CN="' . $at['name'] . '"';
- $organizer .= ":mailto:". $at['email'] . self::EOL;
- }
- }
- else if ($at['email']) {
- //I am an attendee
- $attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
- if ($at['rsvp'])
- $attendees .= ";RSVP=TRUE";
- if (!empty($at['name']))
- $attendees .= ';CN="' . $at['name'] . '"';
- $attendees .= ":mailto:" . $at['email'] . self::EOL;
- }
- }
-
- return $organizer . $attendees;
- }
-
-}
diff --git a/plugins/libcalendaring/lib/Horde_Date.php b/plugins/libcalendaring/lib/Horde_Date.php
new file mode 100644
index 0000000..9197f84
--- /dev/null
+++ b/plugins/libcalendaring/lib/Horde_Date.php
@@ -0,0 +1,1304 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ * Horde/Date/Utils.php, Horde/Date/Recurrence.php
+ * Pull the latest version of these files from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package Date
+ *
+ * @TODO in format():
+ * http://php.net/intldateformatter
+ *
+ * @TODO on timezones:
+ * http://trac.agavi.org/ticket/1008
+ * http://trac.agavi.org/changeset/3659
+ *
+ * @TODO on switching to PHP::DateTime:
+ * The only thing ever stored in the database *IS* Unix timestamps. Doing
+ * anything other than that is unmanageable, yet some frameworks use 'server
+ * based' times in their systems, simply because they do not bother with
+ * daylight saving and only 'serve' one timezone!
+ *
+ * The second you have to manage 'real' time across timezones then daylight
+ * saving becomes essential, BUT only on the display side! Since the browser
+ * only provides a time offset, this is useless and to be honest should simply
+ * be ignored ( until it is upgraded to provide the correct information ;)
+ * ). So we need a 'display' function that takes a simple numeric epoch, and a
+ * separate timezone id into which the epoch is to be 'converted'. My W3C
+ * mapping works simply because ADOdb then converts that to it's own simple
+ * offset abbreviation - in my case GMT or BST. As long as DateTime passes the
+ * full 64 bit number the date range from 100AD is also preserved ( and
+ * further back if 2 digit years are disabled ). If I want to display the
+ * 'real' timezone with this 'time' then I just add it in place of ADOdb's
+ * 'timezone'. I am tempted to simply adjust the ADOdb class to take a
+ * timezone in place of the simple GMT switch it currently uses.
+ *
+ * The return path is just the reverse and simply needs to take the client
+ * display offset off prior to storage of the UTC epoch. SO we use
+ * DateTimeZone to get an offset value for the clients timezone and simply add
+ * or subtract this from a timezone agnostic display on the client end when
+ * entering new times.
+ *
+ *
+ * It's not really feasible to store dates in specific timezone, as most
+ * national/local timezones support DST - and that is a pain to support, as
+ * eg. sorting breaks when some timestamps get repeated. That's why it's
+ * usually better to store datetimes as either UTC datetime or plain unix
+ * timestamp. I usually go with the former - using database datetime type.
+ */
+
+/**
+ * @category Horde
+ * @package Date
+ */
+class Horde_Date
+{
+ const DATE_SUNDAY = 0;
+ const DATE_MONDAY = 1;
+ const DATE_TUESDAY = 2;
+ const DATE_WEDNESDAY = 3;
+ const DATE_THURSDAY = 4;
+ const DATE_FRIDAY = 5;
+ const DATE_SATURDAY = 6;
+
+ const MASK_SUNDAY = 1;
+ const MASK_MONDAY = 2;
+ const MASK_TUESDAY = 4;
+ const MASK_WEDNESDAY = 8;
+ const MASK_THURSDAY = 16;
+ const MASK_FRIDAY = 32;
+ const MASK_SATURDAY = 64;
+ const MASK_WEEKDAYS = 62;
+ const MASK_WEEKEND = 65;
+ const MASK_ALLDAYS = 127;
+
+ const MASK_SECOND = 1;
+ const MASK_MINUTE = 2;
+ const MASK_HOUR = 4;
+ const MASK_DAY = 8;
+ const MASK_MONTH = 16;
+ const MASK_YEAR = 32;
+ const MASK_ALLPARTS = 63;
+
+ const DATE_DEFAULT = 'Y-m-d H:i:s';
+ const DATE_JSON = 'Y-m-d\TH:i:s';
+
+ /**
+ * Year
+ *
+ * @var integer
+ */
+ protected $_year;
+
+ /**
+ * Month
+ *
+ * @var integer
+ */
+ protected $_month;
+
+ /**
+ * Day
+ *
+ * @var integer
+ */
+ protected $_mday;
+
+ /**
+ * Hour
+ *
+ * @var integer
+ */
+ protected $_hour = 0;
+
+ /**
+ * Minute
+ *
+ * @var integer
+ */
+ protected $_min = 0;
+
+ /**
+ * Second
+ *
+ * @var integer
+ */
+ protected $_sec = 0;
+
+ /**
+ * String representation of the date's timezone.
+ *
+ * @var string
+ */
+ protected $_timezone;
+
+ /**
+ * Default format for __toString()
+ *
+ * @var string
+ */
+ protected $_defaultFormat = self::DATE_DEFAULT;
+
+ /**
+ * Default specs that are always supported.
+ * @var string
+ */
+ protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
+
+ /**
+ * Internally supported strftime() specifiers.
+ * @var string
+ */
+ protected static $_supportedSpecs = '';
+
+ /**
+ * Map of required correction masks.
+ *
+ * @see __set()
+ *
+ * @var array
+ */
+ protected static $_corrections = array(
+ 'year' => self::MASK_YEAR,
+ 'month' => self::MASK_MONTH,
+ 'mday' => self::MASK_DAY,
+ 'hour' => self::MASK_HOUR,
+ 'min' => self::MASK_MINUTE,
+ 'sec' => self::MASK_SECOND,
+ );
+
+ protected $_formatCache = array();
+
+ /**
+ * Builds a new date object. If $date contains date parts, use them to
+ * initialize the object.
+ *
+ * Recognized formats:
+ * - arrays with keys 'year', 'month', 'mday', 'day'
+ * 'hour', 'min', 'minute', 'sec'
+ * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+ * - yyyy-mm-dd hh:mm:ss
+ * - yyyymmddhhmmss
+ * - yyyymmddThhmmssZ
+ * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
+ * 03 Mar 1973)
+ * - unix timestamps
+ * - anything parsed by strtotime()/DateTime.
+ *
+ * @throws Horde_Date_Exception
+ */
+ public function __construct($date = null, $timezone = null)
+ {
+ if (!self::$_supportedSpecs) {
+ self::$_supportedSpecs = self::$_defaultSpecs;
+ if (function_exists('nl_langinfo')) {
+ self::$_supportedSpecs .= 'bBpxX';
+ }
+ }
+
+ if (func_num_args() > 2) {
+ // Handle args in order: year month day hour min sec tz
+ $this->_initializeFromArgs(func_get_args());
+ return;
+ }
+
+ $this->_initializeTimezone($timezone);
+
+ if (is_null($date)) {
+ return;
+ }
+
+ if (is_string($date)) {
+ $date = trim($date, '"');
+ }
+
+ if (is_object($date)) {
+ $this->_initializeFromObject($date);
+ } elseif (is_array($date)) {
+ $this->_initializeFromArray($date);
+ } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $date, $parts)) {
+ $this->_year = (int)$parts[1];
+ $this->_month = (int)$parts[2];
+ $this->_mday = (int)$parts[3];
+ $this->_hour = (int)$parts[4];
+ $this->_min = (int)$parts[5];
+ $this->_sec = (int)$parts[6];
+ if ($parts[7]) {
+ $this->_initializeTimezone('UTC');
+ }
+ } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
+ $parts[2] > 0 && $parts[2] <= 12 &&
+ $parts[3] > 0 && $parts[3] <= 31) {
+ $this->_year = (int)$parts[1];
+ $this->_month = (int)$parts[2];
+ $this->_mday = (int)$parts[3];
+ $this->_hour = $this->_min = $this->_sec = 0;
+ } elseif ((string)(int)$date == $date) {
+ // Try as a timestamp.
+ $parts = @getdate($date);
+ if ($parts) {
+ $this->_year = $parts['year'];
+ $this->_month = $parts['mon'];
+ $this->_mday = $parts['mday'];
+ $this->_hour = $parts['hours'];
+ $this->_min = $parts['minutes'];
+ $this->_sec = $parts['seconds'];
+ }
+ } else {
+ // Use date_create() so we can catch errors with PHP 5.2. Use
+ // "new DateTime() once we require 5.3.
+ $parsed = date_create($date);
+ if (!$parsed) {
+ throw new Horde_Date_Exception(sprintf(Horde_Date_Translation::t("Failed to parse time string (%s)"), $date));
+ }
+ $parsed->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ $this->_year = (int)$parsed->format('Y');
+ $this->_month = (int)$parsed->format('m');
+ $this->_mday = (int)$parsed->format('d');
+ $this->_hour = (int)$parsed->format('H');
+ $this->_min = (int)$parsed->format('i');
+ $this->_sec = (int)$parsed->format('s');
+ $this->_initializeTimezone(date_default_timezone_get());
+ }
+ }
+
+ /**
+ * Returns a simple string representation of the date object
+ *
+ * @return string This object converted to a string.
+ */
+ public function __toString()
+ {
+ try {
+ return $this->format($this->_defaultFormat);
+ } catch (Exception $e) {
+ return '';
+ }
+ }
+
+ /**
+ * Returns a DateTime object representing this object.
+ *
+ * @return DateTime
+ */
+ public function toDateTime()
+ {
+ $date = new DateTime(null, new DateTimeZone($this->_timezone));
+ $date->setDate($this->_year, $this->_month, $this->_mday);
+ $date->setTime($this->_hour, $this->_min, $this->_sec);
+ return $date;
+ }
+
+ /**
+ * Converts a date in the proleptic Gregorian calendar to the no of days
+ * since 24th November, 4714 B.C.
+ *
+ * Returns the no of days since Monday, 24th November, 4714 B.C. in the
+ * proleptic Gregorian calendar (which is 24th November, -4713 using
+ * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
+ * proleptic Julian calendar). This is also the first day of the 'Julian
+ * Period' proposed by Joseph Scaliger in 1583, and the number of days
+ * since this date is known as the 'Julian Day'. (It is not directly
+ * to do with the Julian calendar, although this is where the name
+ * is derived from.)
+ *
+ * The algorithm is valid for all years (positive and negative), and
+ * also for years preceding 4714 B.C.
+ *
+ * Algorithm is from PEAR::Date_Calc
+ *
+ * @author Monte Ohrt <monte at ispi.net>
+ * @author Pierre-Alain Joye <pajoye at php.net>
+ * @author Daniel Convissor <danielc at php.net>
+ * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
+ *
+ * @return integer The number of days since 24th November, 4714 B.C.
+ */
+ public function toDays()
+ {
+ if (function_exists('GregorianToJD')) {
+ return gregoriantojd($this->_month, $this->_mday, $this->_year);
+ }
+
+ $day = $this->_mday;
+ $month = $this->_month;
+ $year = $this->_year;
+
+ if ($month > 2) {
+ // March = 0, April = 1, ..., December = 9,
+ // January = 10, February = 11
+ $month -= 3;
+ } else {
+ $month += 9;
+ --$year;
+ }
+
+ $hb_negativeyear = $year < 0;
+ $century = intval($year / 100);
+ $year = $year % 100;
+
+ if ($hb_negativeyear) {
+ // Subtract 1 because year 0 is a leap year;
+ // And N.B. that we must treat the leap years as occurring
+ // one year earlier than they do, because for the purposes
+ // of calculation, the year starts on 1st March:
+ //
+ return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
+ intval((1461 * $year + 1) / 4) +
+ intval((153 * $month + 2) / 5) +
+ $day + 1721118;
+ } else {
+ return intval(146097 * $century / 4) +
+ intval(1461 * $year / 4) +
+ intval((153 * $month + 2) / 5) +
+ $day + 1721119;
+ }
+ }
+
+ /**
+ * Converts number of days since 24th November, 4714 B.C. (in the proleptic
+ * Gregorian calendar, which is year -4713 using 'Astronomical' year
+ * numbering) to Gregorian calendar date.
+ *
+ * Returned date belongs to the proleptic Gregorian calendar, using
+ * 'Astronomical' year numbering.
+ *
+ * The algorithm is valid for all years (positive and negative), and
+ * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
+ * and so the only limitation is platform-dependent (for 32-bit systems
+ * the maximum year would be something like about 1,465,190 A.D.).
+ *
+ * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
+ *
+ * Algorithm is from PEAR::Date_Calc
+ *
+ * @author Monte Ohrt <monte at ispi.net>
+ * @author Pierre-Alain Joye <pajoye at php.net>
+ * @author Daniel Convissor <danielc at php.net>
+ * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
+ *
+ * @param int $days the number of days since 24th November, 4714 B.C.
+ * @param string $format the string indicating how to format the output
+ *
+ * @return Horde_Date A Horde_Date object representing the date.
+ */
+ public static function fromDays($days)
+ {
+ if (function_exists('JDToGregorian')) {
+ list($month, $day, $year) = explode('/', JDToGregorian($days));
+ } else {
+ $days = intval($days);
+
+ $days -= 1721119;
+ $century = floor((4 * $days - 1) / 146097);
+ $days = floor(4 * $days - 1 - 146097 * $century);
+ $day = floor($days / 4);
+
+ $year = floor((4 * $day + 3) / 1461);
+ $day = floor(4 * $day + 3 - 1461 * $year);
+ $day = floor(($day + 4) / 4);
+
+ $month = floor((5 * $day - 3) / 153);
+ $day = floor(5 * $day - 3 - 153 * $month);
+ $day = floor(($day + 5) / 5);
+
+ $year = $century * 100 + $year;
+ if ($month < 10) {
+ $month +=3;
+ } else {
+ $month -=9;
+ ++$year;
+ }
+ }
+
+ return new Horde_Date($year, $month, $day);
+ }
+
+ /**
+ * Getter for the date and time properties.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ *
+ * @return integer The property value, or null if not set.
+ */
+ public function __get($name)
+ {
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+
+ return $this->{'_' . $name};
+ }
+
+ /**
+ * Setter for the date and time properties.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ * @param integer $value The property value.
+ */
+ public function __set($name, $value)
+ {
+ if ($name == 'timezone') {
+ $this->_initializeTimezone($value);
+ return;
+ }
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+
+ if ($name != 'year' && $name != 'month' && $name != 'mday' &&
+ $name != 'hour' && $name != 'min' && $name != 'sec') {
+ throw new InvalidArgumentException('Undefined property ' . $name);
+ }
+
+ $down = $value < $this->{'_' . $name};
+ $this->{'_' . $name} = $value;
+ $this->_correct(self::$_corrections[$name], $down);
+ $this->_formatCache = array();
+ }
+
+ /**
+ * Returns whether a date or time property exists.
+ *
+ * @param string $name One of 'year', 'month', 'mday', 'hour', 'min' or
+ * 'sec'.
+ *
+ * @return boolen True if the property exists and is set.
+ */
+ public function __isset($name)
+ {
+ if ($name == 'day') {
+ $name = 'mday';
+ }
+ return ($name == 'year' || $name == 'month' || $name == 'mday' ||
+ $name == 'hour' || $name == 'min' || $name == 'sec') &&
+ isset($this->{'_' . $name});
+ }
+
+ /**
+ * Adds a number of seconds or units to this date, returning a new Date
+ * object.
+ */
+ public function add($factor)
+ {
+ $d = clone($this);
+ if (is_array($factor) || is_object($factor)) {
+ foreach ($factor as $property => $value) {
+ $d->$property += $value;
+ }
+ } else {
+ $d->sec += $factor;
+ }
+
+ return $d;
+ }
+
+ /**
+ * Subtracts a number of seconds or units from this date, returning a new
+ * Horde_Date object.
+ */
+ public function sub($factor)
+ {
+ if (is_array($factor)) {
+ foreach ($factor as &$value) {
+ $value *= -1;
+ }
+ } else {
+ $factor *= -1;
+ }
+
+ return $this->add($factor);
+ }
+
+ /**
+ * Converts this object to a different timezone.
+ *
+ * @param string $timezone The new timezone.
+ *
+ * @return Horde_Date This object.
+ */
+ public function setTimezone($timezone)
+ {
+ $date = $this->toDateTime();
+ $date->setTimezone(new DateTimeZone($timezone));
+ $this->_timezone = $timezone;
+ $this->_year = (int)$date->format('Y');
+ $this->_month = (int)$date->format('m');
+ $this->_mday = (int)$date->format('d');
+ $this->_hour = (int)$date->format('H');
+ $this->_min = (int)$date->format('i');
+ $this->_sec = (int)$date->format('s');
+ $this->_formatCache = array();
+ return $this;
+ }
+
+ /**
+ * Sets the default date format used in __toString()
+ *
+ * @param string $format
+ */
+ public function setDefaultFormat($format)
+ {
+ $this->_defaultFormat = $format;
+ }
+
+ /**
+ * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
+ *
+ * @return integer The day of the week.
+ */
+ public function dayOfWeek()
+ {
+ if ($this->_month > 2) {
+ $month = $this->_month - 2;
+ $year = $this->_year;
+ } else {
+ $month = $this->_month + 10;
+ $year = $this->_year - 1;
+ }
+
+ $day = (floor((13 * $month - 1) / 5) +
+ $this->_mday + ($year % 100) +
+ floor(($year % 100) / 4) +
+ floor(($year / 100) / 4) - 2 *
+ floor($year / 100) + 77);
+
+ return (int)($day - 7 * floor($day / 7));
+ }
+
+ /**
+ * Returns the day number of the year (1 to 365/366).
+ *
+ * @return integer The day of the year.
+ */
+ public function dayOfYear()
+ {
+ return $this->format('z') + 1;
+ }
+
+ /**
+ * Returns the week of the month.
+ *
+ * @return integer The week number.
+ */
+ public function weekOfMonth()
+ {
+ return ceil($this->_mday / 7);
+ }
+
+ /**
+ * Returns the week of the year, first Monday is first day of first week.
+ *
+ * @return integer The week number.
+ */
+ public function weekOfYear()
+ {
+ return $this->format('W');
+ }
+
+ /**
+ * Returns the number of weeks in the given year (52 or 53).
+ *
+ * @param integer $year The year to count the number of weeks in.
+ *
+ * @return integer $numWeeks The number of weeks in $year.
+ */
+ public static function weeksInYear($year)
+ {
+ // Find the last Thursday of the year.
+ $date = new Horde_Date($year . '-12-31');
+ while ($date->dayOfWeek() != self::DATE_THURSDAY) {
+ --$date->mday;
+ }
+ return $date->weekOfYear();
+ }
+
+ /**
+ * Sets the date of this object to the $nth weekday of $weekday.
+ *
+ * @param integer $weekday The day of the week (0 = Sunday, etc).
+ * @param integer $nth The $nth $weekday to set to (defaults to 1).
+ */
+ public function setNthWeekday($weekday, $nth = 1)
+ {
+ if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
+ return;
+ }
+
+ if ($nth < 0) { // last $weekday of month
+ $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ $last = $this->dayOfWeek();
+ $this->_mday += ($weekday - $last);
+ if ($this->_mday > $lastday)
+ $this->_mday -= 7;
+ }
+ else {
+ $this->_mday = 1;
+ $first = $this->dayOfWeek();
+ if ($weekday < $first) {
+ $this->_mday = 8 + $weekday - $first;
+ } else {
+ $this->_mday = $weekday - $first + 1;
+ }
+ $diff = 7 * $nth - 7;
+ $this->_mday += $diff;
+ $this->_correct(self::MASK_DAY, $diff < 0);
+ }
+ }
+
+ /**
+ * Is the date currently represented by this object a valid date?
+ *
+ * @return boolean Validity, counting leap years, etc.
+ */
+ public function isValid()
+ {
+ return ($this->_year >= 0 && $this->_year <= 9999);
+ }
+
+ /**
+ * Compares this date to another date object to see which one is
+ * greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are on the same date
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ public function compareDate($other)
+ {
+ if (!($other instanceof Horde_Date)) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($this->_year != $other->year) {
+ return $this->_year - $other->year;
+ }
+ if ($this->_month != $other->month) {
+ return $this->_month - $other->month;
+ }
+
+ return $this->_mday - $other->mday;
+ }
+
+ /**
+ * Returns whether this date is after the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is after the other.
+ */
+ public function after($other)
+ {
+ return $this->compareDate($other) > 0;
+ }
+
+ /**
+ * Returns whether this date is before the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is before the other.
+ */
+ public function before($other)
+ {
+ return $this->compareDate($other) < 0;
+ }
+
+ /**
+ * Returns whether this date is the same like the other.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return boolean True if this date is the same like the other.
+ */
+ public function equals($other)
+ {
+ return $this->compareDate($other) == 0;
+ }
+
+ /**
+ * Compares this to another date object by time, to see which one
+ * is greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are at the same time
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ public function compareTime($other)
+ {
+ if (!($other instanceof Horde_Date)) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($this->_hour != $other->hour) {
+ return $this->_hour - $other->hour;
+ }
+ if ($this->_min != $other->min) {
+ return $this->_min - $other->min;
+ }
+
+ return $this->_sec - $other->sec;
+ }
+
+ /**
+ * Compares this to another date object, including times, to see
+ * which one is greater (later). Assumes that the dates are in the
+ * same timezone.
+ *
+ * @param mixed $other The date to compare to.
+ *
+ * @return integer == 0 if they are equal
+ * >= 1 if $this is greater (later)
+ * <= -1 if $other is greater (later)
+ */
+ public function compareDateTime($other)
+ {
+ if (!($other instanceof Horde_Date)) {
+ $other = new Horde_Date($other);
+ }
+
+ if ($diff = $this->compareDate($other)) {
+ return $diff;
+ }
+
+ return $this->compareTime($other);
+ }
+
+ /**
+ * Returns number of days between this date and another.
+ *
+ * @param Horde_Date $other The other day to diff with.
+ *
+ * @return integer The absolute number of days between the two dates.
+ */
+ public function diff($other)
+ {
+ return abs($this->toDays() - $other->toDays());
+ }
+
+ /**
+ * Returns the time offset for local time zone.
+ *
+ * @param boolean $colon Place a colon between hours and minutes?
+ *
+ * @return string Timezone offset as a string in the format +HH:MM.
+ */
+ public function tzOffset($colon = true)
+ {
+ return $colon ? $this->format('P') : $this->format('O');
+ }
+
+ /**
+ * Returns the unix timestamp representation of this date.
+ *
+ * @return integer A unix timestamp.
+ */
+ public function timestamp()
+ {
+ if ($this->_year >= 1970 && $this->_year < 2038) {
+ return mktime($this->_hour, $this->_min, $this->_sec,
+ $this->_month, $this->_mday, $this->_year);
+ }
+ return $this->format('U');
+ }
+
+ /**
+ * Returns the unix timestamp representation of this date, 12:00am.
+ *
+ * @return integer A unix timestamp.
+ */
+ public function datestamp()
+ {
+ if ($this->_year >= 1970 && $this->_year < 2038) {
+ return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
+ }
+ $date = new DateTime($this->format('Y-m-d'));
+ return $date->format('U');
+ }
+
+ /**
+ * Formats date and time to be passed around as a short url parameter.
+ *
+ * @return string Date and time.
+ */
+ public function dateString()
+ {
+ return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
+ }
+
+ /**
+ * Formats date and time to the ISO format used by JSON.
+ *
+ * @return string Date and time.
+ */
+ public function toJson()
+ {
+ return $this->format(self::DATE_JSON);
+ }
+
+ /**
+ * Formats date and time to the RFC 2445 iCalendar DATE-TIME format.
+ *
+ * @param boolean $floating Whether to return a floating date-time
+ * (without time zone information).
+ *
+ * @return string Date and time.
+ */
+ public function toiCalendar($floating = false)
+ {
+ if ($floating) {
+ return $this->format('Ymd\THis');
+ }
+ $dateTime = $this->toDateTime();
+ $dateTime->setTimezone(new DateTimeZone('UTC'));
+ return $dateTime->format('Ymd\THis\Z');
+ }
+
+ /**
+ * Formats time using the specifiers available in date() or in the DateTime
+ * class' format() method.
+ *
+ * To format in languages other than English, use strftime() instead.
+ *
+ * @param string $format
+ *
+ * @return string Formatted time.
+ */
+ public function format($format)
+ {
+ if (!isset($this->_formatCache[$format])) {
+ $this->_formatCache[$format] = $this->toDateTime()->format($format);
+ }
+ return $this->_formatCache[$format];
+ }
+
+ /**
+ * Formats date and time using strftime() format.
+ *
+ * @return string strftime() formatted date and time.
+ */
+ public function strftime($format)
+ {
+ if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
+ return strftime($format, $this->timestamp());
+ } else {
+ return $this->_strftime($format);
+ }
+ }
+
+ /**
+ * Formats date and time using a limited set of the strftime() format.
+ *
+ * @return string strftime() formatted date and time.
+ */
+ protected function _strftime($format)
+ {
+ return preg_replace(
+ array('/%b/e',
+ '/%B/e',
+ '/%C/e',
+ '/%d/e',
+ '/%D/e',
+ '/%e/e',
+ '/%H/e',
+ '/%I/e',
+ '/%m/e',
+ '/%M/e',
+ '/%n/',
+ '/%p/e',
+ '/%R/e',
+ '/%S/e',
+ '/%t/',
+ '/%T/e',
+ '/%x/e',
+ '/%X/e',
+ '/%y/e',
+ '/%Y/',
+ '/%%/'),
+ array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
+ '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
+ '(int)($this->_year / 100)',
+ 'sprintf(\'%02d\', $this->_mday)',
+ '$this->_strftime(\'%m/%d/%y\')',
+ 'sprintf(\'%2d\', $this->_mday)',
+ 'sprintf(\'%02d\', $this->_hour)',
+ 'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
+ 'sprintf(\'%02d\', $this->_month)',
+ 'sprintf(\'%02d\', $this->_min)',
+ "\n",
+ '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
+ '$this->_strftime(\'%H:%M\')',
+ 'sprintf(\'%02d\', $this->_sec)',
+ "\t",
+ '$this->_strftime(\'%H:%M:%S\')',
+ '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))',
+ '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))',
+ 'substr(sprintf(\'%04d\', $this->_year), -2)',
+ (int)$this->_year,
+ '%'),
+ $format);
+ }
+
+ /**
+ * Corrects any over- or underflows in any of the date's members.
+ *
+ * @param integer $mask We may not want to correct some overflows.
+ * @param integer $down Whether to correct the date up or down.
+ */
+ protected function _correct($mask = self::MASK_ALLPARTS, $down = false)
+ {
+ if ($mask & self::MASK_SECOND) {
+ if ($this->_sec < 0 || $this->_sec > 59) {
+ $mask |= self::MASK_MINUTE;
+
+ $this->_min += (int)($this->_sec / 60);
+ $this->_sec %= 60;
+ if ($this->_sec < 0) {
+ $this->_min--;
+ $this->_sec += 60;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_MINUTE) {
+ if ($this->_min < 0 || $this->_min > 59) {
+ $mask |= self::MASK_HOUR;
+
+ $this->_hour += (int)($this->_min / 60);
+ $this->_min %= 60;
+ if ($this->_min < 0) {
+ $this->_hour--;
+ $this->_min += 60;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_HOUR) {
+ if ($this->_hour < 0 || $this->_hour > 23) {
+ $mask |= self::MASK_DAY;
+
+ $this->_mday += (int)($this->_hour / 24);
+ $this->_hour %= 24;
+ if ($this->_hour < 0) {
+ $this->_mday--;
+ $this->_hour += 24;
+ }
+ }
+ }
+
+ if ($mask & self::MASK_MONTH) {
+ $this->_correctMonth($down);
+ /* When correcting the month, always correct the day too. Months
+ * have different numbers of days. */
+ $mask |= self::MASK_DAY;
+ }
+
+ if ($mask & self::MASK_DAY) {
+ while ($this->_mday > 28 &&
+ $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) {
+ if ($down) {
+ $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ } else {
+ $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ $this->_month++;
+ }
+ $this->_correctMonth($down);
+ }
+ while ($this->_mday < 1) {
+ --$this->_month;
+ $this->_correctMonth($down);
+ $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+ }
+ }
+ }
+
+ /**
+ * Corrects the current month.
+ *
+ * This cannot be done in _correct() because that would also trigger a
+ * correction of the day, which would result in an infinite loop.
+ *
+ * @param integer $down Whether to correct the date up or down.
+ */
+ protected function _correctMonth($down = false)
+ {
+ $this->_year += (int)($this->_month / 12);
+ $this->_month %= 12;
+ if ($this->_month < 1) {
+ $this->_year--;
+ $this->_month += 12;
+ }
+ }
+
+ /**
+ * Handles args in order: year month day hour min sec tz
+ */
+ protected function _initializeFromArgs($args)
+ {
+ $tz = (isset($args[6])) ? array_pop($args) : null;
+ $this->_initializeTimezone($tz);
+
+ $args = array_slice($args, 0, 6);
+ $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
+ $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
+ $date = array_merge($keys, $date);
+
+ $this->_initializeFromArray($date);
+ }
+
+ protected function _initializeFromArray($date)
+ {
+ if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
+ if ($date['year'] > 70) {
+ $date['year'] += 1900;
+ } else {
+ $date['year'] += 2000;
+ }
+ }
+
+ foreach ($date as $key => $val) {
+ if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
+ $this->{'_'. $key} = (int)$val;
+ }
+ }
+
+ // If $date['day'] is present and numeric we may have been passed
+ // a Horde_Form_datetime array.
+ if (isset($date['day']) &&
+ (string)(int)$date['day'] == $date['day']) {
+ $this->_mday = (int)$date['day'];
+ }
+ // 'minute' key also from Horde_Form_datetime
+ if (isset($date['minute']) &&
+ (string)(int)$date['minute'] == $date['minute']) {
+ $this->_min = (int)$date['minute'];
+ }
+
+ $this->_correct();
+ }
+
+ protected function _initializeFromObject($date)
+ {
+ if ($date instanceof DateTime) {
+ $this->_year = (int)$date->format('Y');
+ $this->_month = (int)$date->format('m');
+ $this->_mday = (int)$date->format('d');
+ $this->_hour = (int)$date->format('H');
+ $this->_min = (int)$date->format('i');
+ $this->_sec = (int)$date->format('s');
+ $this->_initializeTimezone($date->getTimezone()->getName());
+ } else {
+ $is_horde_date = $date instanceof Horde_Date;
+ foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
+ if ($is_horde_date || isset($date->$key)) {
+ $this->{'_' . $key} = (int)$date->$key;
+ }
+ }
+ if (!$is_horde_date) {
+ $this->_correct();
+ } else {
+ $this->_initializeTimezone($date->timezone);
+ }
+ }
+ }
+
+ protected function _initializeTimezone($timezone)
+ {
+ if (empty($timezone)) {
+ $timezone = date_default_timezone_get();
+ }
+ $this->_timezone = $timezone;
+ }
+
+}
+
+/**
+ * @category Horde
+ * @package Date
+ */
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package Date
+ */
+class Horde_Date_Utils
+{
+ /**
+ * Returns whether a year is a leap year.
+ *
+ * @param integer $year The year.
+ *
+ * @return boolean True if the year is a leap year.
+ */
+ public static function isLeapYear($year)
+ {
+ if (strlen($year) != 4 || preg_match('/\D/', $year)) {
+ return false;
+ }
+
+ return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
+ }
+
+ /**
+ * Returns the date of the year that corresponds to the first day of the
+ * given week.
+ *
+ * @param integer $week The week of the year to find the first day of.
+ * @param integer $year The year to calculate for.
+ *
+ * @return Horde_Date The date of the first day of the given week.
+ */
+ public static function firstDayOfWeek($week, $year)
+ {
+ return new Horde_Date(sprintf('%04dW%02d', $year, $week));
+ }
+
+ /**
+ * Returns the number of days in the specified month.
+ *
+ * @param integer $month The month
+ * @param integer $year The year.
+ *
+ * @return integer The number of days in the month.
+ */
+ public static function daysInMonth($month, $year)
+ {
+ static $cache = array();
+ if (!isset($cache[$year][$month])) {
+ $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
+ $cache[$year][$month] = $date->format('t');
+ }
+ return $cache[$year][$month];
+ }
+
+ /**
+ * Returns a relative, natural language representation of a timestamp
+ *
+ * @todo Wider range of values ... maybe future time as well?
+ * @todo Support minimum resolution parameter.
+ *
+ * @param mixed $time The time. Any format accepted by Horde_Date.
+ * @param string $date_format Format to display date if timestamp is
+ * more then 1 day old.
+ * @param string $time_format Format to display time if timestamp is 1
+ * day old.
+ *
+ * @return string The relative time (i.e. 2 minutes ago)
+ */
+ public static function relativeDateTime($time, $date_format = '%x',
+ $time_format = '%X')
+ {
+ $date = new Horde_Date($time);
+
+ $delta = time() - $date->timestamp();
+ if ($delta < 60) {
+ return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta);
+ }
+
+ $delta = round($delta / 60);
+ if ($delta < 60) {
+ return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta);
+ }
+
+ $delta = round($delta / 60);
+ if ($delta < 24) {
+ return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta);
+ }
+
+ if ($delta > 24 && $delta < 48) {
+ $date = new Horde_Date($time);
+ return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format));
+ }
+
+ $delta = round($delta / 24);
+ if ($delta < 7) {
+ return sprintf(Horde_Date_Translation::t("%d days ago"), $delta);
+ }
+
+ if (round($delta / 7) < 5) {
+ $delta = round($delta / 7);
+ return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta);
+ }
+
+ // Default to the user specified date format.
+ return $date->strftime($date_format);
+ }
+
+ /**
+ * Tries to convert strftime() formatters to date() formatters.
+ *
+ * Unsupported formatters will be removed.
+ *
+ * @param string $format A strftime() formatting string.
+ *
+ * @return string A date() formatting string.
+ */
+ public static function strftime2date($format)
+ {
+ $replace = array(
+ '/%a/' => 'D',
+ '/%A/' => 'l',
+ '/%d/' => 'd',
+ '/%e/' => 'j',
+ '/%j/' => 'z',
+ '/%u/' => 'N',
+ '/%w/' => 'w',
+ '/%U/' => '',
+ '/%V/' => 'W',
+ '/%W/' => '',
+ '/%b/' => 'M',
+ '/%B/' => 'F',
+ '/%h/' => 'M',
+ '/%m/' => 'm',
+ '/%C/' => '',
+ '/%g/' => '',
+ '/%G/' => 'o',
+ '/%y/' => 'y',
+ '/%Y/' => 'Y',
+ '/%H/' => 'H',
+ '/%I/' => 'h',
+ '/%i/' => 'g',
+ '/%M/' => 'i',
+ '/%p/' => 'A',
+ '/%P/' => 'a',
+ '/%r/' => 'h:i:s A',
+ '/%R/' => 'H:i',
+ '/%S/' => 's',
+ '/%T/' => 'H:i:s',
+ '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))',
+ '/%z/' => 'O',
+ '/%Z/' => '',
+ '/%c/' => '',
+ '/%D/' => 'm/d/y',
+ '/%F/' => 'Y-m-d',
+ '/%s/' => 'U',
+ '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))',
+ '/%n/' => "\n",
+ '/%t/' => "\t",
+ '/%%/' => '%'
+ );
+
+ return preg_replace(array_keys($replace), array_values($replace), $format);
+ }
+
+}
diff --git a/plugins/libcalendaring/lib/Horde_iCalendar.php b/plugins/libcalendaring/lib/Horde_iCalendar.php
new file mode 100644
index 0000000..6d75d27
--- /dev/null
+++ b/plugins/libcalendaring/lib/Horde_iCalendar.php
@@ -0,0 +1,3300 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ * Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ */
+
+if (!class_exists('Horde_Date'))
+ require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+
+$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
+
+/**
+ * The String:: class provides static methods for charset and locale safe
+ * string manipulation.
+ *
+ * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Jan Schneider <jan at horde.org>
+ * @since Horde 3.0
+ * @package Horde_Util
+ */
+class String {
+
+ /**
+ * Caches the result of extension_loaded() calls.
+ *
+ * @param string $ext The extension name.
+ *
+ * @return boolean Is the extension loaded?
+ *
+ * @see Util::extensionExists()
+ */
+ function extensionExists($ext)
+ {
+ static $cache = array();
+
+ if (!isset($cache[$ext])) {
+ $cache[$ext] = extension_loaded($ext);
+ }
+
+ return $cache[$ext];
+ }
+
+ /**
+ * Sets a default charset that the String:: methods will use if none is
+ * explicitly specified.
+ *
+ * @param string $charset The charset to use as the default one.
+ */
+ function setDefaultCharset($charset)
+ {
+ $GLOBALS['_HORDE_STRING_CHARSET'] = $charset;
+ if (String::extensionExists('mbstring') &&
+ function_exists('mb_regex_encoding')) {
+ $old_error = error_reporting(0);
+ mb_regex_encoding(String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ }
+ }
+
+ /**
+ * Converts a string from one charset to another.
+ *
+ * Works only if either the iconv or the mbstring extension
+ * are present and best if both are available.
+ * The original string is returned if conversion failed or none
+ * of the extensions were available.
+ *
+ * @param mixed $input The data to be converted. If $input is an an array,
+ * the array's values get converted recursively.
+ * @param string $from The string's current charset.
+ * @param string $to The charset to convert the string to. If not
+ * specified, the global variable
+ * $_HORDE_STRING_CHARSET will be used.
+ *
+ * @return mixed The converted input data.
+ */
+ function convertCharset($input, $from, $to = null)
+ {
+ /* Don't bother converting numbers. */
+ if (is_numeric($input)) {
+ return $input;
+ }
+
+ /* Get the user's default character set if none passed in. */
+ if (is_null($to)) {
+ $to = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+
+ /* If the from and to character sets are identical, return now. */
+ if ($from == $to) {
+ return $input;
+ }
+ $from = String::lower($from);
+ $to = String::lower($to);
+ if ($from == $to) {
+ return $input;
+ }
+
+ if (is_array($input)) {
+ $tmp = array();
+ reset($input);
+ while (list($key, $val) = each($input)) {
+ $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to);
+ }
+ return $tmp;
+ }
+ if (is_object($input)) {
+ // PEAR_Error objects are almost guaranteed to contain recursion,
+ // which will cause a segfault in PHP. We should never reach
+ // this line, but add a check and a log message to help the devs
+ // track down and fix this issue.
+ if (is_a($input, 'PEAR_Error')) {
+ Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+ return '';
+ }
+ $vars = get_object_vars($input);
+ while (list($key, $val) = each($vars)) {
+ $input->$key = String::convertCharset($val, $from, $to);
+ }
+ return $input;
+ }
+
+ if (!is_string($input)) {
+ return $input;
+ }
+
+ return String::_convertCharset($input, $from, $to);
+ }
+
+ /**
+ * Internal function used to do charset conversion.
+ *
+ * @access private
+ *
+ * @param string $input See String::convertCharset().
+ * @param string $from See String::convertCharset().
+ * @param string $to See String::convertCharset().
+ *
+ * @return string The converted string.
+ */
+ function _convertCharset($input, $from, $to)
+ {
+ $output = '';
+ $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii'));
+ $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii'));
+
+ /* Use utf8_[en|de]code() if possible and if the string isn't too
+ * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these
+ * functions use more memory. */
+ if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) {
+ if ($from_check && ($to == 'utf-8')) {
+ return utf8_encode($input);
+ }
+
+ if (($from == 'utf-8') && $to_check) {
+ return utf8_decode($input);
+ }
+ }
+
+ /* First try iconv with transliteration. */
+ if (($from != 'utf7-imap') &&
+ ($to != 'utf7-imap') &&
+ String::extensionExists('iconv')) {
+ /* We need to tack an extra character temporarily because of a bug
+ * in iconv() if the last character is not a 7 bit ASCII
+ * character. */
+ $oldTrackErrors = ini_set('track_errors', 1);
+ unset($php_errormsg);
+ $output = @iconv($from, $to . '//TRANSLIT', $input . 'x');
+ $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to);
+ ini_set('track_errors', $oldTrackErrors);
+ }
+
+ /* Next try mbstring. */
+ if (!$output && String::extensionExists('mbstring')) {
+ $old_error = error_reporting(0);
+ $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from));
+ error_reporting($old_error);
+ }
+
+ /* At last try imap_utf7_[en|de]code if appropriate. */
+ if (!$output && String::extensionExists('imap')) {
+ if ($from_check && ($to == 'utf7-imap')) {
+ return @imap_utf7_encode($input);
+ }
+ if (($from == 'utf7-imap') && $to_check) {
+ return @imap_utf7_decode($input);
+ }
+ }
+
+ return (!$output) ? $input : $output;
+ }
+
+ /**
+ * Makes a string lowercase.
+ *
+ * @param string $string The string to be converted.
+ * @param boolean $locale If true the string will be converted based on a
+ * given charset, locale independent else.
+ * @param string $charset If $locale is true, the charset to use when
+ * converting. If not provided the current charset.
+ *
+ * @return string The string with lowercase characters
+ */
+ function lower($string, $locale = false, $charset = null)
+ {
+ static $lowers;
+
+ if ($locale) {
+ /* The existence of mb_strtolower() depends on the platform. */
+ if (String::extensionExists('mbstring') &&
+ function_exists('mb_strtolower')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $old_error = error_reporting(0);
+ $ret = mb_strtolower($string, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ if (!empty($ret)) {
+ return $ret;
+ }
+ }
+ return strtolower($string);
+ }
+
+ if (!isset($lowers)) {
+ $lowers = array();
+ }
+ if (!isset($lowers[$string])) {
+ $language = setlocale(LC_CTYPE, 0);
+ setlocale(LC_CTYPE, 'C');
+ $lowers[$string] = strtolower($string);
+ setlocale(LC_CTYPE, $language);
+ }
+
+ return $lowers[$string];
+ }
+
+ /**
+ * Makes a string uppercase.
+ *
+ * @param string $string The string to be converted.
+ * @param boolean $locale If true the string will be converted based on a
+ * given charset, locale independent else.
+ * @param string $charset If $locale is true, the charset to use when
+ * converting. If not provided the current charset.
+ *
+ * @return string The string with uppercase characters
+ */
+ function upper($string, $locale = false, $charset = null)
+ {
+ static $uppers;
+
+ if ($locale) {
+ /* The existence of mb_strtoupper() depends on the
+ * platform. */
+ if (function_exists('mb_strtoupper')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $old_error = error_reporting(0);
+ $ret = mb_strtoupper($string, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ if (!empty($ret)) {
+ return $ret;
+ }
+ }
+ return strtoupper($string);
+ }
+
+ if (!isset($uppers)) {
+ $uppers = array();
+ }
+ if (!isset($uppers[$string])) {
+ $language = setlocale(LC_CTYPE, 0);
+ setlocale(LC_CTYPE, 'C');
+ $uppers[$string] = strtoupper($string);
+ setlocale(LC_CTYPE, $language);
+ }
+
+ return $uppers[$string];
+ }
+
+ /**
+ * Returns a string with the first letter capitalized if it is
+ * alphabetic.
+ *
+ * @param string $string The string to be capitalized.
+ * @param boolean $locale If true the string will be converted based on a
+ * given charset, locale independent else.
+ * @param string $charset The charset to use, defaults to current charset.
+ *
+ * @return string The capitalized string.
+ */
+ function ucfirst($string, $locale = false, $charset = null)
+ {
+ if ($locale) {
+ $first = String::substr($string, 0, 1, $charset);
+ if (String::isAlpha($first, $charset)) {
+ $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset);
+ }
+ } else {
+ $string = String::upper(substr($string, 0, 1), false) . substr($string, 1);
+ }
+ return $string;
+ }
+
+ /**
+ * Returns part of a string.
+ *
+ * @param string $string The string to be converted.
+ * @param integer $start The part's start position, zero based.
+ * @param integer $length The part's length.
+ * @param string $charset The charset to use when calculating the part's
+ * position and length, defaults to current
+ * charset.
+ *
+ * @return string The string's part.
+ */
+ function substr($string, $start, $length = null, $charset = null)
+ {
+ if (is_null($length)) {
+ $length = String::length($string, $charset) - $start;
+ }
+
+ if ($length == 0) {
+ return '';
+ }
+
+ /* Try iconv. */
+ if (function_exists('iconv_substr')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+
+ $old_error = error_reporting(0);
+ $ret = iconv_substr($string, $start, $length, $charset);
+ error_reporting($old_error);
+ /* iconv_substr() returns false on failure. */
+ if ($ret !== false) {
+ return $ret;
+ }
+ }
+
+ /* Try mbstring. */
+ if (String::extensionExists('mbstring')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $old_error = error_reporting(0);
+ $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ /* mb_substr() returns empty string on failure. */
+ if (strlen($ret)) {
+ return $ret;
+ }
+ }
+
+ return substr($string, $start, $length);
+ }
+
+ /**
+ * Returns the character (not byte) length of a string.
+ *
+ * @param string $string The string to return the length of.
+ * @param string $charset The charset to use when calculating the string's
+ * length.
+ *
+ * @return string The string's part.
+ */
+ function length($string, $charset = null)
+ {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $charset = String::lower($charset);
+ if ($charset == 'utf-8' || $charset == 'utf8') {
+ return strlen(utf8_decode($string));
+ }
+ if (String::extensionExists('mbstring')) {
+ $old_error = error_reporting(0);
+ $ret = mb_strlen($string, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ if (!empty($ret)) {
+ return $ret;
+ }
+ }
+ return strlen($string);
+ }
+
+ /**
+ * Returns the numeric position of the first occurrence of $needle
+ * in the $haystack string.
+ *
+ * @param string $haystack The string to search through.
+ * @param string $needle The string to search for.
+ * @param integer $offset Allows to specify which character in haystack
+ * to start searching.
+ * @param string $charset The charset to use when searching for the
+ * $needle string.
+ *
+ * @return integer The position of first occurrence.
+ */
+ function pos($haystack, $needle, $offset = 0, $charset = null)
+ {
+ if (String::extensionExists('mbstring')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $track_errors = ini_set('track_errors', 1);
+ $old_error = error_reporting(0);
+ $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ ini_set('track_errors', $track_errors);
+ if (!isset($php_errormsg)) {
+ return $ret;
+ }
+ }
+ return strpos($haystack, $needle, $offset);
+ }
+
+ /**
+ * Returns a string padded to a certain length with another string.
+ *
+ * This method behaves exactly like str_pad but is multibyte safe.
+ *
+ * @param string $input The string to be padded.
+ * @param integer $length The length of the resulting string.
+ * @param string $pad The string to pad the input string with. Must
+ * be in the same charset like the input string.
+ * @param const $type The padding type. One of STR_PAD_LEFT,
+ * STR_PAD_RIGHT, or STR_PAD_BOTH.
+ * @param string $charset The charset of the input and the padding
+ * strings.
+ *
+ * @return string The padded string.
+ */
+ function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT,
+ $charset = null)
+ {
+ $mb_length = String::length($input, $charset);
+ $sb_length = strlen($input);
+ $pad_length = String::length($pad, $charset);
+
+ /* Return if we already have the length. */
+ if ($mb_length >= $length) {
+ return $input;
+ }
+
+ /* Shortcut for single byte strings. */
+ if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
+ return str_pad($input, $length, $pad, $type);
+ }
+
+ switch ($type) {
+ case STR_PAD_LEFT:
+ $left = $length - $mb_length;
+ $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input;
+ break;
+ case STR_PAD_BOTH:
+ $left = floor(($length - $mb_length) / 2);
+ $right = ceil(($length - $mb_length) / 2);
+ $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) .
+ $input .
+ String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+ break;
+ case STR_PAD_RIGHT:
+ $right = $length - $mb_length;
+ $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+ break;
+ }
+
+ return $output;
+ }
+
+ /**
+ * Wraps the text of a message.
+ *
+ * @since Horde 3.2
+ *
+ * @param string $string String containing the text to wrap.
+ * @param integer $width Wrap the string at this number of
+ * characters.
+ * @param string $break Character(s) to use when breaking lines.
+ * @param boolean $cut Whether to cut inside words if a line
+ * can't be wrapped.
+ * @param string $charset Character set to use when breaking lines.
+ * @param boolean $line_folding Whether to apply line folding rules per
+ * RFC 822 or similar. The correct break
+ * characters including leading whitespace
+ * have to be specified too.
+ *
+ * @return string String containing the wrapped text.
+ */
+ function wordwrap($string, $width = 75, $break = "\n", $cut = false,
+ $charset = null, $line_folding = false)
+ {
+ /* Get the user's default character set if none passed in. */
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $charset = String::_mbstringCharset($charset);
+ $string = String::convertCharset($string, $charset, 'utf-8');
+ $wrapped = '';
+
+ while (String::length($string, 'utf-8') > $width) {
+ $line = String::substr($string, 0, $width, 'utf-8');
+ $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8');
+ // Make sure didn't cut a word, unless we want hard breaks anyway.
+ if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) {
+ $line .= $match[1];
+ $string = $match[2];
+ }
+ // Wrap at existing line breaks.
+ if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) {
+ $wrapped .= $match[1] . $match[2];
+ $string = $match[3] . $string;
+ continue;
+ }
+ // Wrap at the last colon or semicolon followed by a whitespace if
+ // doing line folding.
+ if ($line_folding &&
+ preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) {
+ $wrapped .= $match[1] . $match[2] . $break;
+ $string = $match[3] . $string;
+ continue;
+ }
+ // Wrap at the last whitespace of $line.
+ if ($line_folding) {
+ $sub = '(.+[^\s])';
+ } else {
+ $sub = '(.*)';
+ }
+ if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) {
+ $wrapped .= $match[1] . $break;
+ $string = ($line_folding ? $match[2] : '') . $match[3] . $string;
+ continue;
+ }
+ // Hard wrap if necessary.
+ if ($cut) {
+ $wrapped .= $line . $break;
+ continue;
+ }
+ $wrapped .= $line;
+ }
+
+ return String::convertCharset($wrapped . $string, 'utf-8', $charset);
+ }
+
+ /**
+ * Wraps the text of a message.
+ *
+ * @param string $text String containing the text to wrap.
+ * @param integer $length Wrap $text at this number of characters.
+ * @param string $break_char Character(s) to use when breaking lines.
+ * @param string $charset Character set to use when breaking lines.
+ * @param boolean $quote Ignore lines that are wrapped with the '>'
+ * character (RFC 2646)? If true, we don't
+ * remove any padding whitespace at the end of
+ * the string.
+ *
+ * @return string String containing the wrapped text.
+ */
+ function wrap($text, $length = 80, $break_char = "\n", $charset = null,
+ $quote = false)
+ {
+ $paragraphs = array();
+
+ foreach (preg_split('/\r?\n/', $text) as $input) {
+ if ($quote && (strpos($input, '>') === 0)) {
+ $line = $input;
+ } else {
+ /* We need to handle the Usenet-style signature line
+ * separately; since the space after the two dashes is
+ * REQUIRED, we don't want to trim the line. */
+ if ($input != '-- ') {
+ $input = rtrim($input);
+ }
+ $line = String::wordwrap($input, $length, $break_char, false, $charset);
+ }
+
+ $paragraphs[] = $line;
+ }
+
+ return implode($break_char, $paragraphs);
+ }
+
+ /**
+ * Returns true if the every character in the parameter is an alphabetic
+ * character.
+ *
+ * @param $string The string to test.
+ * @param $charset The charset to use when testing the string.
+ *
+ * @return boolean True if the parameter was alphabetic only.
+ */
+ function isAlpha($string, $charset = null)
+ {
+ if (!String::extensionExists('mbstring')) {
+ return ctype_alpha($string);
+ }
+
+ $charset = String::_mbstringCharset($charset);
+ $old_charset = mb_regex_encoding();
+ $old_error = error_reporting(0);
+
+ if ($charset != $old_charset) {
+ mb_regex_encoding($charset);
+ }
+ $alpha = !mb_ereg_match('[^[:alpha:]]', $string);
+ if ($charset != $old_charset) {
+ mb_regex_encoding($old_charset);
+ }
+
+ error_reporting($old_error);
+
+ return $alpha;
+ }
+
+ /**
+ * Returns true if ever character in the parameter is a lowercase letter in
+ * the current locale.
+ *
+ * @param $string The string to test.
+ * @param $charset The charset to use when testing the string.
+ *
+ * @return boolean True if the parameter was lowercase.
+ */
+ function isLower($string, $charset = null)
+ {
+ return ((String::lower($string, true, $charset) === $string) &&
+ String::isAlpha($string, $charset));
+ }
+
+ /**
+ * Returns true if every character in the parameter is an uppercase letter
+ * in the current locale.
+ *
+ * @param string $string The string to test.
+ * @param string $charset The charset to use when testing the string.
+ *
+ * @return boolean True if the parameter was uppercase.
+ */
+ function isUpper($string, $charset = null)
+ {
+ return ((String::upper($string, true, $charset) === $string) &&
+ String::isAlpha($string, $charset));
+ }
+
+ /**
+ * Performs a multibyte safe regex match search on the text provided.
+ *
+ * @since Horde 3.1
+ *
+ * @param string $text The text to search.
+ * @param array $regex The regular expressions to use, without perl
+ * regex delimiters (e.g. '/' or '|').
+ * @param string $charset The character set of the text.
+ *
+ * @return array The matches array from the first regex that matches.
+ */
+ function regexMatch($text, $regex, $charset = null)
+ {
+ if (!empty($charset)) {
+ $regex = String::convertCharset($regex, $charset, 'utf-8');
+ $text = String::convertCharset($text, $charset, 'utf-8');
+ }
+
+ $matches = array();
+ foreach ($regex as $val) {
+ if (preg_match('/' . $val . '/u', $text, $matches)) {
+ break;
+ }
+ }
+
+ if (!empty($charset)) {
+ $matches = String::convertCharset($matches, 'utf-8', $charset);
+ }
+
+ return $matches;
+ }
+
+ /**
+ * Workaround charsets that don't work with mbstring functions.
+ *
+ * @access private
+ *
+ * @param string $charset The original charset.
+ *
+ * @return string The charset to use with mbstring functions.
+ */
+ function _mbstringCharset($charset)
+ {
+ /* mbstring functions do not handle the 'ks_c_5601-1987' &
+ * 'ks_c_5601-1989' charsets. However, these charsets are used, for
+ * example, by various versions of Outlook to send Korean characters.
+ * Use UHC (CP949) encoding instead. See, e.g.,
+ * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */
+ if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) {
+ $charset = 'UHC';
+ }
+
+ return $charset;
+ }
+
+}
+
+
+
+/**
+ * @package Horde_iCalendar
+ */
+
+/**
+ * String package
+ */
+
+
+
+/**
+ * Class representing iCalendar files.
+ *
+ * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar {
+
+ /**
+ * The parent (containing) iCalendar object.
+ *
+ * @var Horde_iCalendar
+ */
+ var $_container = false;
+
+ /**
+ * The name/value pairs of attributes for this object (UID,
+ * DTSTART, etc.). Which are present depends on the object and on
+ * what kind of component it is.
+ *
+ * @var array
+ */
+ var $_attributes = array();
+
+ /**
+ * Any children (contained) iCalendar components of this object.
+ *
+ * @var array
+ */
+ var $_components = array();
+
+ /**
+ * According to RFC 2425, we should always use CRLF-terminated lines.
+ *
+ * @var string
+ */
+ var $_newline = "\r\n";
+
+ /**
+ * iCalendar format version (different behavior for 1.0 and 2.0
+ * especially with recurring events).
+ *
+ * @var string
+ */
+ var $_version;
+
+ function Horde_iCalendar($version = '2.0')
+ {
+ $this->_version = $version;
+ $this->setAttribute('VERSION', $version);
+ }
+
+ /**
+ * Return a reference to a new component.
+ *
+ * @param string $type The type of component to return
+ * @param Horde_iCalendar $container A container that this component
+ * will be associated with.
+ *
+ * @return object Reference to a Horde_iCalendar_* object as specified.
+ *
+ * @static
+ */
+ function &newComponent($type, &$container)
+ {
+ $type = String::lower($type);
+ $class = 'Horde_iCalendar_' . $type;
+ if (!class_exists($class)) {
+ include 'Horde/iCalendar/' . $type . '.php';
+ }
+ if (class_exists($class)) {
+ $component = new $class();
+ if ($container !== false) {
+ $component->_container = &$container;
+ // Use version of container, not default set by component
+ // constructor.
+ $component->_version = $container->_version;
+ }
+ } else {
+ // Should return an dummy x-unknown type class here.
+ $component = false;
+ }
+
+ return $component;
+ }
+
+ /**
+ * Sets the value of an attribute.
+ *
+ * @param string $name The name of the attribute.
+ * @param string $value The value of the attribute.
+ * @param array $params Array containing any addition parameters for
+ * this attribute.
+ * @param boolean $append True to append the attribute, False to replace
+ * the first matching attribute found.
+ * @param array $values Array representation of $value. For
+ * comma/semicolon seperated lists of values. If
+ * not set use $value as single array element.
+ */
+ function setAttribute($name, $value, $params = array(), $append = true,
+ $values = false)
+ {
+ // Make sure we update the internal format version if
+ // setAttribute('VERSION', ...) is called.
+ if ($name == 'VERSION') {
+ $this->_version = $value;
+ if ($this->_container !== false) {
+ $this->_container->_version = $value;
+ }
+ }
+
+ if (!$values) {
+ $values = array($value);
+ }
+ $found = false;
+ if (!$append) {
+ foreach (array_keys($this->_attributes) as $key) {
+ if ($this->_attributes[$key]['name'] == String::upper($name)) {
+ $this->_attributes[$key]['params'] = $params;
+ $this->_attributes[$key]['value'] = $value;
+ $this->_attributes[$key]['values'] = $values;
+ $found = true;
+ break;
+ }
+ }
+ }
+
+ if ($append || !$found) {
+ $this->_attributes[] = array(
+ 'name' => String::upper($name),
+ 'params' => $params,
+ 'value' => $value,
+ 'values' => $values
+ );
+ }
+ }
+
+ /**
+ * Sets parameter(s) for an (already existing) attribute. The
+ * parameter set is merged into the existing set.
+ *
+ * @param string $name The name of the attribute.
+ * @param array $params Array containing any additional parameters for
+ * this attribute.
+ * @return boolean True on success, false if no attribute $name exists.
+ */
+ function setParameter($name, $params = array())
+ {
+ $keys = array_keys($this->_attributes);
+ foreach ($keys as $key) {
+ if ($this->_attributes[$key]['name'] == $name) {
+ $this->_attributes[$key]['params'] =
+ array_merge($this->_attributes[$key]['params'], $params);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the value of an attribute.
+ *
+ * @param string $name The name of the attribute.
+ * @param boolean $params Return the parameters for this attribute instead
+ * of its value.
+ *
+ * @return mixed (object) PEAR_Error if the attribute does not exist.
+ * (string) The value of the attribute.
+ * (array) The parameters for the attribute or
+ * multiple values for an attribute.
+ */
+ function getAttribute($name, $params = false)
+ {
+ $result = array();
+ foreach ($this->_attributes as $attribute) {
+ if ($attribute['name'] == $name) {
+ if ($params) {
+ $result[] = $attribute['params'];
+ } else {
+ $result[] = $attribute['value'];
+ }
+ }
+ }
+ if (!count($result)) {
+ require_once 'PEAR.php';
+ return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+ } if (count($result) == 1 && !$params) {
+ return $result[0];
+ } else {
+ return $result;
+ }
+ }
+
+ /**
+ * Gets the values of an attribute as an array. Multiple values
+ * are possible due to:
+ *
+ * a) multiplce occurences of 'name'
+ * b) (unsecapd) comma seperated lists.
+ *
+ * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
+ * will return array('a', 'b', 'c').
+ *
+ * @param string $name The name of the attribute.
+ * @return mixed (object) PEAR_Error if the attribute does not exist.
+ * (array) Multiple values for an attribute.
+ */
+ function getAttributeValues($name)
+ {
+ $result = array();
+ foreach ($this->_attributes as $attribute) {
+ if ($attribute['name'] == $name) {
+ $result = array_merge($attribute['values'], $result);
+ }
+ }
+ if (!count($result)) {
+ return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+ }
+ return $result;
+ }
+
+ /**
+ * Returns the value of an attribute, or a specified default value
+ * if the attribute does not exist.
+ *
+ * @param string $name The name of the attribute.
+ * @param mixed $default What to return if the attribute specified by
+ * $name does not exist.
+ *
+ * @return mixed (string) The value of $name.
+ * (mixed) $default if $name does not exist.
+ */
+ function getAttributeDefault($name, $default = '')
+ {
+ $value = $this->getAttribute($name);
+ return is_a($value, 'PEAR_Error') ? $default : $value;
+ }
+
+ /**
+ * Remove all occurences of an attribute.
+ *
+ * @param string $name The name of the attribute.
+ */
+ function removeAttribute($name)
+ {
+ $keys = array_keys($this->_attributes);
+ foreach ($keys as $key) {
+ if ($this->_attributes[$key]['name'] == $name) {
+ unset($this->_attributes[$key]);
+ }
+ }
+ }
+
+ /**
+ * Get attributes for all tags or for a given tag.
+ *
+ * @param string $tag Return attributes for this tag, or all attributes if
+ * not given.
+ *
+ * @return array An array containing all the attributes and their types.
+ */
+ function getAllAttributes($tag = false)
+ {
+ if ($tag === false) {
+ return $this->_attributes;
+ }
+ $result = array();
+ foreach ($this->_attributes as $attribute) {
+ if ($attribute['name'] == $tag) {
+ $result[] = $attribute;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Add a vCalendar component (eg vEvent, vTimezone, etc.).
+ *
+ * @param Horde_iCalendar $component Component (subclass) to add.
+ */
+ function addComponent($component)
+ {
+ if (is_a($component, 'Horde_iCalendar')) {
+ $component->_container = &$this;
+ $this->_components[] = &$component;
+ }
+ }
+
+ /**
+ * Retrieve all the components.
+ *
+ * @return array Array of Horde_iCalendar objects.
+ */
+ function getComponents()
+ {
+ return $this->_components;
+ }
+
+ function getType()
+ {
+ return 'vcalendar';
+ }
+
+ /**
+ * Return the classes (entry types) we have.
+ *
+ * @return array Hash with class names Horde_iCalendar_xxx as keys
+ * and number of components of this class as value.
+ */
+ function getComponentClasses()
+ {
+ $r = array();
+ foreach ($this->_components as $c) {
+ $cn = strtolower(get_class($c));
+ if (empty($r[$cn])) {
+ $r[$cn] = 1;
+ } else {
+ $r[$cn]++;
+ }
+ }
+
+ return $r;
+ }
+
+ /**
+ * Number of components in this container.
+ *
+ * @return integer Number of components in this container.
+ */
+ function getComponentCount()
+ {
+ return count($this->_components);
+ }
+
+ /**
+ * Retrieve a specific component.
+ *
+ * @param integer $idx The index of the object to retrieve.
+ *
+ * @return mixed (boolean) False if the index does not exist.
+ * (Horde_iCalendar_*) The requested component.
+ */
+ function getComponent($idx)
+ {
+ if (isset($this->_components[$idx])) {
+ return $this->_components[$idx];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Locates the first child component of the specified class, and returns a
+ * reference to it.
+ *
+ * @param string $type The type of component to find.
+ *
+ * @return boolean|Horde_iCalendar_* False if no subcomponent of the
+ * specified class exists or a reference
+ * to the requested component.
+ */
+ function &findComponent($childclass)
+ {
+ $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+ $keys = array_keys($this->_components);
+ foreach ($keys as $key) {
+ if (is_a($this->_components[$key], $childclass)) {
+ return $this->_components[$key];
+ }
+ }
+
+ $component = false;
+ return $component;
+ }
+
+ /**
+ * Locates the first matching child component of the specified class, and
+ * returns a reference to it.
+ *
+ * @param string $childclass The type of component to find.
+ * @param string $attribute This attribute must be set in the component
+ * for it to match.
+ * @param string $value Optional value that $attribute must match.
+ *
+ * @return boolean|Horde_iCalendar_* False if no matching subcomponent of
+ * the specified class exists, or a
+ * reference to the requested component.
+ */
+ function &findComponentByAttribute($childclass, $attribute, $value = null)
+ {
+ $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+ $keys = array_keys($this->_components);
+ foreach ($keys as $key) {
+ if (is_a($this->_components[$key], $childclass)) {
+ $attr = $this->_components[$key]->getAttribute($attribute);
+ if (is_a($attr, 'PEAR_Error')) {
+ continue;
+ }
+ if ($value !== null && $value != $attr) {
+ continue;
+ }
+ return $this->_components[$key];
+ }
+ }
+
+ $component = false;
+ return $component;
+ }
+
+ /**
+ * Clears the iCalendar object (resets the components and attributes
+ * arrays).
+ */
+ function clear()
+ {
+ $this->_components = array();
+ $this->_attributes = array();
+ }
+
+ /**
+ * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
+ *
+ * These 'old' formats are defined by www.imc.org. The 'new' (non-old)
+ * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
+ * respectively.
+ *
+ * @since Horde 3.1.2
+ */
+ function isOldFormat()
+ {
+ if ($this->getType() == 'vcard') {
+ return ($this->_version < 3);
+ }
+ if ($this->getType() == 'vNote') {
+ return ($this->_version < 2);
+ }
+ if ($this->_version >= 2) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Export as vCalendar format.
+ */
+ function exportvCalendar()
+ {
+ // Default values.
+ $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
+ $requiredAttributes['METHOD'] = 'PUBLISH';
+
+ foreach ($requiredAttributes as $name => $default_value) {
+ if (is_a($this->getattribute($name), 'PEAR_Error')) {
+ $this->setAttribute($name, $default_value);
+ }
+ }
+
+ return $this->_exportvData('VCALENDAR');
+ }
+
+ /**
+ * Export this entry as a hash array with tag names as keys.
+ *
+ * @param boolean $paramsInKeys
+ * If false, the operation can be quite lossy as the
+ * parameters are ignored when building the array keys.
+ * So if you export a vcard with
+ * LABEL;TYPE=WORK:foo
+ * LABEL;TYPE=HOME:bar
+ * the resulting hash contains only one label field!
+ * If set to true, array keys look like 'LABEL;TYPE=WORK'
+ * @return array A hash array with tag names as keys.
+ */
+ function toHash($paramsInKeys = false)
+ {
+ $hash = array();
+ foreach ($this->_attributes as $a) {
+ $k = $a['name'];
+ if ($paramsInKeys && is_array($a['params'])) {
+ foreach ($a['params'] as $p => $v) {
+ $k .= ";$p=$v";
+ }
+ }
+ $hash[$k] = $a['value'];
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Parses a string containing vCalendar data.
+ *
+ * @todo This method doesn't work well at all, if $base is VCARD.
+ *
+ * @param string $text The data to parse.
+ * @param string $base The type of the base object.
+ * @param string $charset The encoding charset for $text. Defaults to
+ * utf-8 for new format, iso-8859-1 for old format.
+ * @param boolean $clear If true clears the iCal object before parsing.
+ *
+ * @return boolean True on successful import, false otherwise.
+ */
+ function parsevCalendar($text, $base = 'VCALENDAR', $charset = null,
+ $clear = true)
+ {
+ if ($clear) {
+ $this->clear();
+ }
+ if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
+ $container = true;
+ $vCal = $matches[1];
+ } else {
+ // Text isn't enclosed in BEGIN:VCALENDAR
+ // .. END:VCALENDAR. We'll try to parse it anyway.
+ $container = false;
+ $vCal = $text;
+ }
+ $vCal = trim($vCal);
+
+ // Extract all subcomponents.
+ $matches = $components = null;
+ if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) {
+ foreach ($components[0] as $key => $data) {
+ // Remove from the vCalendar data.
+ $vCal = str_replace($data, '', $vCal);
+ }
+ } elseif (!$container) {
+ return false;
+ }
+
+ // Unfold "quoted printable" folded lines like:
+ // BODY;ENCODING=QUOTED-PRINTABLE:=
+ // another=20line=
+ // last=20line
+ while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) {
+ foreach ($matches[1] as $s) {
+ $r = preg_replace('/=\r?\n/', '', $s);
+ $vCal = str_replace($s, $r, $vCal);
+ }
+ }
+
+ // Unfold any folded lines.
+ if ($this->isOldFormat()) {
+ $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal);
+ } else {
+ $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
+ }
+
+ // Parse the remaining attributes.
+ if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
+ foreach ($matches[0] as $attribute) {
+ preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
+ $tag = trim(String::upper($parts[1]));
+ $value = $parts[4];
+ $params = array();
+
+ // Parse parameters.
+ if (!empty($parts[2])) {
+ preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
+ foreach ($param_parts[2] as $key => $paramName) {
+ $paramName = String::upper($paramName);
+ $paramValue = $param_parts[4][$key];
+ if ($paramName == 'TYPE') {
+ $paramValue = preg_split('/(?<!\\\\),/', $paramValue);
+ if (count($paramValue) == 1) {
+ $paramValue = $paramValue[0];
+ }
+ }
+ if (is_string($paramValue)) {
+ if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
+ $paramValue = $parts[1];
+ }
+ } else {
+ foreach ($paramValue as $k => $tmp) {
+ if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
+ $paramValue[$k] = $parts[1];
+ }
+ }
+ }
+ $params[$paramName] = $paramValue;
+ }
+ }
+
+ // Charset and encoding handling.
+ if ((isset($params['ENCODING']) &&
+ String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
+ isset($params['QUOTED-PRINTABLE'])) {
+
+ $value = quoted_printable_decode($value);
+ if (isset($params['CHARSET'])) {
+ $value = String::convertCharset($value, $params['CHARSET']);
+ } else {
+ $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+ }
+ } elseif (isset($params['CHARSET'])) {
+ $value = String::convertCharset($value, $params['CHARSET']);
+ } else {
+ // As per RFC 2279, assume UTF8 if we don't have an
+ // explicit charset parameter.
+ $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+ }
+
+ // Get timezone info for date fields from $params.
+ $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
+
+ switch ($tag) {
+ // Date fields.
+ case 'COMPLETED':
+ case 'CREATED':
+ case 'LAST-MODIFIED':
+ case 'X-MOZ-LASTACK':
+ case 'X-MOZ-SNOOZE-TIME':
+ $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+ break;
+
+ case 'BDAY':
+ case 'X-SYNCJE-ANNIVERSARY':
+ case 'X-ANNIVERSARY':
+ $this->setAttribute($tag, $this->_parseDate($value), $params);
+ break;
+
+ case 'DTEND':
+ case 'DTSTART':
+ case 'DTSTAMP':
+ case 'DUE':
+ case 'AALARM':
+ case 'RECURRENCE-ID':
+ // types like AALARM may contain additional data after a ;
+ // ignore these.
+ $ts = explode(';', $value);
+ if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
+ $this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
+ } else {
+ $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
+ }
+ break;
+
+ case 'TRIGGER':
+ if (isset($params['VALUE']) &&
+ $params['VALUE'] == 'DATE-TIME') {
+ $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+ } else {
+ $this->setAttribute($tag, $this->_parseDuration($value), $params);
+ }
+ break;
+
+ // Comma seperated dates.
+ case 'EXDATE':
+ case 'RDATE':
+ if (!strlen($value)) {
+ break;
+ }
+ $dates = array();
+ $separator = $this->isOldFormat() ? ';' : ',';
+ preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
+
+ foreach ($values[1] as $value) {
+ $dates[] = $this->_parseDate($value);
+ }
+ $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
+ break;
+
+ // Duration fields.
+ case 'DURATION':
+ $this->setAttribute($tag, $this->_parseDuration($value), $params);
+ break;
+
+ // Period of time fields.
+ case 'FREEBUSY':
+ $periods = array();
+ preg_match_all('/,([^,]*)/', ',' . $value, $values);
+ foreach ($values[1] as $value) {
+ $periods[] = $this->_parsePeriod($value);
+ }
+
+ $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
+ break;
+
+ // UTC offset fields.
+ case 'TZOFFSETFROM':
+ case 'TZOFFSETTO':
+ $this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
+ break;
+
+ // Integer fields.
+ case 'PERCENT-COMPLETE':
+ case 'PRIORITY':
+ case 'REPEAT':
+ case 'SEQUENCE':
+ $this->setAttribute($tag, intval($value), $params);
+ break;
+
+ // Geo fields.
+ case 'GEO':
+ if ($this->isOldFormat()) {
+ $floats = explode(',', $value);
+ $value = array('latitude' => floatval($floats[1]),
+ 'longitude' => floatval($floats[0]));
+ } else {
+ $floats = explode(';', $value);
+ $value = array('latitude' => floatval($floats[0]),
+ 'longitude' => floatval($floats[1]));
+ }
+ $this->setAttribute($tag, $value, $params);
+ break;
+
+ // Recursion fields.
+ case 'EXRULE':
+ case 'RRULE':
+ $this->setAttribute($tag, trim($value), $params);
+ break;
+
+ // ADR, ORG and N are lists seperated by unescaped semicolons
+ // with a specific number of slots.
+ case 'ADR':
+ case 'N':
+ case 'ORG':
+ $value = trim($value);
+ // As of rfc 2426 2.4.2 semicolon, comma, and colon must
+ // be escaped (comma is unescaped after splitting below).
+ $value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
+ array($this->_newline, $this->_newline, ';', ':'),
+ $value);
+
+ // Split by unescaped semicolons:
+ $values = preg_split('/(?<!\\\\);/', $value);
+ $value = str_replace('\\;', ';', $value);
+ $values = str_replace('\\;', ';', $values);
+ $this->setAttribute($tag, trim($value), $params, true, $values);
+ break;
+
+ // String fields.
+ default:
+ if ($this->isOldFormat()) {
+ // vCalendar 1.0 and vCard 2.1 only escape semicolons
+ // and use unescaped semicolons to create lists.
+ $value = trim($value);
+ // Split by unescaped semicolons:
+ $values = preg_split('/(?<!\\\\);/', $value);
+ $value = str_replace('\\;', ';', $value);
+ $values = str_replace('\\;', ';', $values);
+ $this->setAttribute($tag, trim($value), $params, true, $values);
+ } else {
+ $value = trim($value);
+ // As of rfc 2426 2.4.2 semicolon, comma, and colon
+ // must be escaped (comma is unescaped after splitting
+ // below).
+ $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
+ array($this->_newline, $this->_newline, ';', ':', '\\'),
+ $value);
+
+ // Split by unescaped commas.
+ $values = preg_split('/(?<!\\\\),/', $value);
+ $value = str_replace('\\,', ',', $value);
+ $values = str_replace('\\,', ',', $values);
+
+ $this->setAttribute($tag, trim($value), $params, true, $values);
+ }
+ break;
+ }
+ }
+ }
+
+ // Process all components.
+ if ($components) {
+ // vTimezone components are processed first. They are
+ // needed to process vEvents that may use a TZID.
+ foreach ($components[0] as $key => $data) {
+ $type = trim($components[1][$key]);
+ if ($type != 'VTIMEZONE') {
+ continue;
+ }
+ $component = &Horde_iCalendar::newComponent($type, $this);
+ if ($component === false) {
+ return PEAR::raiseError("Unable to create object for type $type");
+ }
+ $component->parsevCalendar($data, $type, $charset);
+
+ $this->addComponent($component);
+ }
+
+ // Now process the non-vTimezone components.
+ foreach ($components[0] as $key => $data) {
+ $type = trim($components[1][$key]);
+ if ($type == 'VTIMEZONE') {
+ continue;
+ }
+ $component = &Horde_iCalendar::newComponent($type, $this);
+ if ($component === false) {
+ return PEAR::raiseError("Unable to create object for type $type");
+ }
+ $component->parsevCalendar($data, $type, $charset);
+
+ $this->addComponent($component);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Export this component in vCal format.
+ *
+ * @param string $base The type of the base object.
+ *
+ * @return string vCal format data.
+ */
+ function _exportvData($base = 'VCALENDAR')
+ {
+ $result = 'BEGIN:' . String::upper($base) . $this->_newline;
+
+ // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
+ // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
+ if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
+ $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') {
+ // Ensure that version is the first attribute.
+ $result .= 'VERSION:' . $this->_version . $this->_newline;
+ }
+ foreach ($this->_attributes as $attribute) {
+ $name = $attribute['name'];
+ if ($name == 'VERSION') {
+ // Already done.
+ continue;
+ }
+
+ $params_str = '';
+ $params = $attribute['params'];
+ if ($params) {
+ foreach ($params as $param_name => $param_value) {
+ /* Skip CHARSET for iCalendar 2.0 data, not allowed. */
+ if ($param_name == 'CHARSET' && !$this->isOldFormat()) {
+ continue;
+ }
+ /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
+ if ($this->isOldFormat() &&
+ $param_name == 'VALUE' && $param_value == 'DATE') {
+ continue;
+ }
+
+ if ($param_value === null) {
+ $params_str .= ";$param_name";
+ } else {
+ $len = strlen($param_value);
+ $safe_value = '';
+ $quote = false;
+ for ($i = 0; $i < $len; ++$i) {
+ $ord = ord($param_value[$i]);
+ // Accept only valid characters.
+ if ($ord == 9 || $ord == 32 || $ord == 33 ||
+ ($ord >= 35 && $ord <= 126) ||
+ $ord >= 128) {
+ $safe_value .= $param_value[$i];
+ // Characters above 128 do not need to be
+ // quoted as per RFC2445 but Outlook requires
+ // this.
+ if ($ord == 44 || $ord == 58 || $ord == 59 ||
+ $ord >= 128) {
+ $quote = true;
+ }
+ }
+ }
+ if ($quote) {
+ $safe_value = '"' . $safe_value . '"';
+ }
+ $params_str .= ";$param_name=$safe_value";
+ }
+ }
+ }
+
+ $value = $attribute['value'];
+ switch ($name) {
+ // Date fields.
+ case 'COMPLETED':
+ case 'CREATED':
+ case 'DCREATED':
+ case 'LAST-MODIFIED':
+ case 'X-MOZ-LASTACK':
+ case 'X-MOZ-SNOOZE-TIME':
+ $value = $this->_exportDateTime($value);
+ break;
+
+ case 'DTEND':
+ case 'DTSTART':
+ case 'DTSTAMP':
+ case 'DUE':
+ case 'AALARM':
+ case 'RECURRENCE-ID':
+ if (isset($params['VALUE'])) {
+ if ($params['VALUE'] == 'DATE') {
+ // VCALENDAR 1.0 uses T000000 - T235959 for all day events:
+ if ($this->isOldFormat() && $name == 'DTEND') {
+ $d = new Horde_Date($value);
+ $value = new Horde_Date(array(
+ 'year' => $d->year,
+ 'month' => $d->month,
+ 'mday' => $d->mday - 1));
+ $value->correct();
+ $value = $this->_exportDate($value, '235959');
+ } else {
+ $value = $this->_exportDate($value, '000000');
+ }
+ } else {
+ $value = $this->_exportDateTime($value);
+ }
+ } else {
+ $value = $this->_exportDateTime($value);
+ }
+ break;
+
+ // Comma seperated dates.
+ case 'EXDATE':
+ case 'RDATE':
+ $dates = array();
+ foreach ($value as $date) {
+ if (isset($params['VALUE'])) {
+ if ($params['VALUE'] == 'DATE') {
+ $dates[] = $this->_exportDate($date, '000000');
+ } elseif ($params['VALUE'] == 'PERIOD') {
+ $dates[] = $this->_exportPeriod($date);
+ } else {
+ $dates[] = $this->_exportDateTime($date);
+ }
+ } else {
+ $dates[] = $this->_exportDateTime($date);
+ }
+ }
+ $value = implode($this->isOldFormat() ? ';' : ',', $dates);
+ break;
+
+ case 'TRIGGER':
+ if (isset($params['VALUE'])) {
+ if ($params['VALUE'] == 'DATE-TIME') {
+ $value = $this->_exportDateTime($value);
+ } elseif ($params['VALUE'] == 'DURATION') {
+ $value = $this->_exportDuration($value);
+ }
+ } else {
+ $value = $this->_exportDuration($value);
+ }
+ break;
+
+ // Duration fields.
+ case 'DURATION':
+ $value = $this->_exportDuration($value);
+ break;
+
+ // Period of time fields.
+ case 'FREEBUSY':
+ $value_str = '';
+ foreach ($value as $period) {
+ $value_str .= empty($value_str) ? '' : ',';
+ $value_str .= $this->_exportPeriod($period);
+ }
+ $value = $value_str;
+ break;
+
+ // UTC offset fields.
+ case 'TZOFFSETFROM':
+ case 'TZOFFSETTO':
+ $value = $this->_exportUtcOffset($value);
+ break;
+
+ // Integer fields.
+ case 'PERCENT-COMPLETE':
+ case 'PRIORITY':
+ case 'REPEAT':
+ case 'SEQUENCE':
+ $value = "$value";
+ break;
+
+ // Geo fields.
+ case 'GEO':
+ if ($this->isOldFormat()) {
+ $value = $value['longitude'] . ',' . $value['latitude'];
+ } else {
+ $value = $value['latitude'] . ';' . $value['longitude'];
+ }
+ break;
+
+ // Recurrence fields.
+ case 'EXRULE':
+ case 'RRULE':
+ break;
+
+ default:
+ if ($this->isOldFormat()) {
+ if (is_array($attribute['values']) &&
+ count($attribute['values']) > 1) {
+ $values = $attribute['values'];
+ if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+ $glue = ';';
+ } else {
+ $glue = ',';
+ }
+ $values = str_replace(';', '\\;', $values);
+ $value = implode($glue, $values);
+ } else {
+ /* vcard 2.1 and vcalendar 1.0 escape only
+ * semicolons */
+ $value = str_replace(';', '\\;', $value);
+ }
+ // Text containing newlines or ASCII >= 127 must be BASE64
+ // or QUOTED-PRINTABLE encoded. Currently we use
+ // QUOTED-PRINTABLE as default.
+ if (preg_match("/[^\x20-\x7F]/", $value) &&
+ empty($params['ENCODING'])) {
+ $params['ENCODING'] = 'QUOTED-PRINTABLE';
+ $params_str .= ';ENCODING=QUOTED-PRINTABLE';
+ // Add CHARSET as well. At least the synthesis client
+ // gets confused otherwise
+ if (empty($params['CHARSET'])) {
+ $params['CHARSET'] = 'UTF-8';
+ $params_str .= ';CHARSET=' . $params['CHARSET'];
+ }
+ }
+ } else {
+ if (is_array($attribute['values']) &&
+ count($attribute['values'])) {
+ $values = $attribute['values'];
+ if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+ $glue = ';';
+ } else {
+ $glue = ',';
+ }
+ // As of rfc 2426 2.5 semicolon and comma must be
+ // escaped.
+ $values = str_replace(array('\\', ';', ','),
+ array('\\\\', '\\;', '\\,'),
+ $values);
+ $value = implode($glue, $values);
+ } else {
+ // As of rfc 2426 2.5 semicolon and comma must be
+ // escaped.
+ $value = str_replace(array('\\', ';', ','),
+ array('\\\\', '\\;', '\\,'),
+ $value);
+ }
+ $value = preg_replace('/\r?\n/', '\n', $value);
+ }
+ break;
+ }
+
+ $value = str_replace("\r", '', $value);
+ if (!empty($params['ENCODING']) &&
+ $params['ENCODING'] == 'QUOTED-PRINTABLE' &&
+ strlen(trim($value))) {
+ $result .= $name . $params_str . ':'
+ . str_replace('=0A', '=0D=0A',
+ $this->_quotedPrintableEncode($value))
+ . $this->_newline;
+ } else {
+ $attr_string = $name . $params_str . ':' . $value;
+ if (!$this->isOldFormat()) {
+ $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ',
+ true, 'utf-8', true);
+ }
+ $result .= $attr_string . $this->_newline;
+ }
+ }
+
+ foreach ($this->_components as $component) {
+ $result .= $component->exportvCalendar();
+ }
+
+ return $result . 'END:' . $base . $this->_newline;
+ }
+
+ /**
+ * Parse a UTC Offset field.
+ */
+ function _parseUtcOffset($text)
+ {
+ $offset = array();
+ if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
+ $offset['ahead'] = (bool)($timeParts[1] == '+');
+ $offset['hour'] = intval($timeParts[2]);
+ $offset['minute'] = intval($timeParts[3]);
+ if (isset($timeParts[4])) {
+ $offset['second'] = intval($timeParts[4]);
+ }
+ return $offset;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Export a UTC Offset field.
+ */
+ function _exportUtcOffset($value)
+ {
+ $offset = $value['ahead'] ? '+' : '-';
+ $offset .= sprintf('%02d%02d',
+ $value['hour'], $value['minute']);
+ if (isset($value['second'])) {
+ $offset .= sprintf('%02d', $value['second']);
+ }
+
+ return $offset;
+ }
+
+ /**
+ * Parse a Time Period field.
+ */
+ function _parsePeriod($text)
+ {
+ $periodParts = explode('/', $text);
+
+ $start = $this->_parseDateTime($periodParts[0]);
+
+ if ($duration = $this->_parseDuration($periodParts[1])) {
+ return array('start' => $start, 'duration' => $duration);
+ } elseif ($end = $this->_parseDateTime($periodParts[1])) {
+ return array('start' => $start, 'end' => $end);
+ }
+ }
+
+ /**
+ * Export a Time Period field.
+ */
+ function _exportPeriod($value)
+ {
+ $period = $this->_exportDateTime($value['start']);
+ $period .= '/';
+ if (isset($value['duration'])) {
+ $period .= $this->_exportDuration($value['duration']);
+ } else {
+ $period .= $this->_exportDateTime($value['end']);
+ }
+ return $period;
+ }
+
+ /**
+ * Grok the TZID and return an offset in seconds from UTC for this
+ * date and time.
+ */
+ function _parseTZID($date, $time, $tzid)
+ {
+ $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
+ if (!$vtimezone) {
+ // use PHP's standard timezone db to determine tzoffset
+ try {
+ $tz = new DateTimeZone($tzid);
+ $dt = new DateTime('now', $tz);
+ $dt->setDate($date['year'], $date['month'], $date['mday']);
+ $dt->setTime($time['hour'], $time['minute'], $date['recond']);
+ return $tz->getOffset($dt);
+ }
+ catch (Exception $e) {
+ return false;
+ }
+ }
+
+ $change_times = array();
+ foreach ($vtimezone->getComponents() as $o) {
+ $t = $vtimezone->parseChild($o, $date['year']);
+ if ($t !== false) {
+ $change_times[] = $t;
+ }
+ }
+
+ if (!$change_times) {
+ return false;
+ }
+
+ sort($change_times);
+
+ // Time is arbitrarily based on UTC for comparison.
+ $t = @gmmktime($time['hour'], $time['minute'], $time['second'],
+ $date['month'], $date['mday'], $date['year']);
+
+ if ($t < $change_times[0]['time']) {
+ return $change_times[0]['from'];
+ }
+
+ for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
+ if (($t >= $change_times[$i]['time']) &&
+ ($t < $change_times[$i + 1]['time'])) {
+ return $change_times[$i]['to'];
+ }
+ }
+
+ if ($t >= $change_times[$n - 1]['time']) {
+ return $change_times[$n - 1]['to'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Parses a DateTime field and returns a unix timestamp. If the
+ * field cannot be parsed then the original text is returned
+ * unmodified.
+ *
+ * @todo This function should be moved to Horde_Date and made public.
+ */
+ function _parseDateTime($text, $tzid = false)
+ {
+ $dateParts = explode('T', $text);
+ if (count($dateParts) != 2 && !empty($text)) {
+ // Not a datetime field but may be just a date field.
+ if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+ // Or not
+ return $text;
+ }
+ $newtext = $text.'T000000';
+ $dateParts = explode('T', $newtext);
+ }
+
+ if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) {
+ return $text;
+ }
+ if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) {
+ return $text;
+ }
+
+ // Get timezone info for date fields from $tzid and container.
+ $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar'))
+ ? $this->_parseTZID($date, $time, $tzid) : false;
+ if ($time['zone'] == 'UTC' || $tzoffset !== false) {
+ $result = @gmmktime($time['hour'], $time['minute'], $time['second'],
+ $date['month'], $date['mday'], $date['year']);
+ if ($tzoffset) {
+ $result -= $tzoffset;
+ }
+ } else {
+ // We don't know the timezone so assume local timezone.
+ // FIXME: shouldn't this be based on the user's timezone
+ // preference rather than the server's timezone?
+ $result = @mktime($time['hour'], $time['minute'], $time['second'],
+ $date['month'], $date['mday'], $date['year']);
+ }
+
+ return ($result !== false) ? $result : $text;
+ }
+
+ /**
+ * Export a DateTime field.
+ */
+ function _exportDateTime($value)
+ {
+ $temp = array();
+ if (!is_object($value) && !is_array($value)) {
+ $tz = date('O', $value);
+ $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2));
+ $value -= $TZOffset;
+
+ $temp['zone'] = 'UTC';
+ list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value));
+ } else {
+ $dateOb = new Horde_Date($value);
+ return Horde_iCalendar::_exportDateTime($dateOb->timestamp());
+ }
+
+ return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp);
+ }
+
+ /**
+ * Parses a Time field.
+ *
+ * @static
+ */
+ function _parseTime($text)
+ {
+ if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
+ $time['hour'] = intval($timeParts[1]);
+ $time['minute'] = intval($timeParts[2]);
+ $time['second'] = intval($timeParts[3]);
+ if (isset($timeParts[4])) {
+ $time['zone'] = 'UTC';
+ } else {
+ $time['zone'] = 'Local';
+ }
+ return $time;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Exports a Time field.
+ */
+ function _exportTime($value)
+ {
+ $time = sprintf('%02d%02d%02d',
+ $value['hour'], $value['minute'], $value['second']);
+ if ($value['zone'] == 'UTC') {
+ $time .= 'Z';
+ }
+ return $time;
+ }
+
+ /**
+ * Parses a Date field.
+ *
+ * @static
+ */
+ function _parseDate($text)
+ {
+ $parts = explode('T', $text);
+ if (count($parts) == 2) {
+ $text = $parts[0];
+ }
+
+ if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+ return false;
+ }
+
+ return array('year' => $match[1],
+ 'month' => $match[2],
+ 'mday' => $match[3]);
+ }
+
+ /**
+ * Exports a date field.
+ *
+ * @param object|array $value Date object or hash.
+ * @param string $autoconvert If set, use this as time part to export the
+ * date as datetime when exporting to Vcalendar
+ * 1.0. Examples: '000000' or '235959'
+ */
+ function _exportDate($value, $autoconvert = false)
+ {
+ if (is_object($value)) {
+ $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
+ }
+ if ($autoconvert !== false && $this->isOldFormat()) {
+ return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert);
+ } else {
+ return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
+ }
+ }
+
+ /**
+ * Parse a Duration Value field.
+ */
+ function _parseDuration($text)
+ {
+ if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
+ // Weeks.
+ $duration = 7 * 86400 * intval($durvalue[3]);
+
+ if (count($durvalue) > 4) {
+ // Days.
+ $duration += 86400 * intval($durvalue[4]);
+ }
+ if (count($durvalue) > 5) {
+ // Hours.
+ $duration += 3600 * intval($durvalue[7]);
+
+ // Mins.
+ if (isset($durvalue[8])) {
+ $duration += 60 * intval($durvalue[8]);
+ }
+
+ // Secs.
+ if (isset($durvalue[9])) {
+ $duration += intval($durvalue[9]);
+ }
+ }
+
+ // Sign.
+ if ($durvalue[1] == "-") {
+ $duration *= -1;
+ }
+
+ return $duration;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Export a duration value.
+ */
+ function _exportDuration($value)
+ {
+ $duration = '';
+ if ($value < 0) {
+ $value *= -1;
+ $duration .= '-';
+ }
+ $duration .= 'P';
+
+ $weeks = floor($value / (7 * 86400));
+ $value = $value % (7 * 86400);
+ if ($weeks) {
+ $duration .= $weeks . 'W';
+ }
+
+ $days = floor($value / (86400));
+ $value = $value % (86400);
+ if ($days) {
+ $duration .= $days . 'D';
+ }
+
+ if ($value) {
+ $duration .= 'T';
+
+ $hours = floor($value / 3600);
+ $value = $value % 3600;
+ if ($hours) {
+ $duration .= $hours . 'H';
+ }
+
+ $mins = floor($value / 60);
+ $value = $value % 60;
+ if ($mins) {
+ $duration .= $mins . 'M';
+ }
+
+ if ($value) {
+ $duration .= $value . 'S';
+ }
+ }
+
+ return $duration;
+ }
+
+ /**
+ * Converts an 8bit string to a quoted-printable string according to RFC
+ * 2045, section 6.7.
+ *
+ * imap_8bit() does not apply all necessary rules.
+ *
+ * @param string $input The string to be encoded.
+ *
+ * @return string The quoted-printable encoded string.
+ */
+ function _quotedPrintableEncode($input = '')
+ {
+ $output = $line = '';
+ $len = strlen($input);
+
+ for ($i = 0; $i < $len; ++$i) {
+ $ord = ord($input[$i]);
+ // Encode non-printable characters (rule 2).
+ if ($ord == 9 ||
+ ($ord >= 32 && $ord <= 60) ||
+ ($ord >= 62 && $ord <= 126)) {
+ $chunk = $input[$i];
+ } else {
+ // Quoted printable encoding (rule 1).
+ $chunk = '=' . String::upper(sprintf('%02X', $ord));
+ }
+ $line .= $chunk;
+ // Wrap long lines (rule 5)
+ if (strlen($line) + 1 > 76) {
+ $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true);
+ $newline = strrchr($line, "\r\n");
+ if ($newline !== false) {
+ $output .= substr($line, 0, -strlen($newline) + 2);
+ $line = substr($newline, 2);
+ } else {
+ $output .= $line;
+ }
+ continue;
+ }
+ // Wrap at line breaks for better readability (rule 4).
+ if (substr($line, -3) == '=0A') {
+ $output .= $line . "=\r\n";
+ $line = '';
+ }
+ }
+ $output .= $line;
+
+ // Trailing whitespace must be encoded (rule 3).
+ $lastpos = strlen($output) - 1;
+ if ($output[$lastpos] == chr(9) ||
+ $output[$lastpos] == chr(32)) {
+ $output[$lastpos] = '=';
+ $output .= String::upper(sprintf('%02X', ord($output[$lastpos])));
+ }
+
+ return $output;
+ }
+
+}
+
+
+
+/**
+ * Class representing vAlarms.
+ *
+ * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_valarm extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vAlarm';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VALARM');
+ }
+
+}
+
+/**
+ * Class representing vEvents.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vevent extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vEvent';
+ }
+
+ function exportvCalendar()
+ {
+ // Default values.
+ $requiredAttributes = array();
+ $requiredAttributes['DTSTAMP'] = time();
+ $requiredAttributes['UID'] = $this->_exportDateTime(time())
+ . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16)
+ . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
+
+ $method = !empty($this->_container) ?
+ $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+ switch ($method) {
+ case 'PUBLISH':
+ $requiredAttributes['DTSTART'] = time();
+ $requiredAttributes['SUMMARY'] = '';
+ break;
+
+ case 'REQUEST':
+ $requiredAttributes['ATTENDEE'] = '';
+ $requiredAttributes['DTSTART'] = time();
+ $requiredAttributes['SUMMARY'] = '';
+ break;
+
+ case 'REPLY':
+ $requiredAttributes['ATTENDEE'] = '';
+ break;
+
+ case 'ADD':
+ $requiredAttributes['DTSTART'] = time();
+ $requiredAttributes['SEQUENCE'] = 1;
+ $requiredAttributes['SUMMARY'] = '';
+ break;
+
+ case 'CANCEL':
+ $requiredAttributes['ATTENDEE'] = '';
+ $requiredAttributes['SEQUENCE'] = 1;
+ break;
+
+ case 'REFRESH':
+ $requiredAttributes['ATTENDEE'] = '';
+ break;
+ }
+
+ foreach ($requiredAttributes as $name => $default_value) {
+ if (is_a($this->getAttribute($name), 'PEAR_Error')) {
+ $this->setAttribute($name, $default_value);
+ }
+ }
+
+ return parent::_exportvData('VEVENT');
+ }
+
+ /**
+ * Update the status of an attendee of an event.
+ *
+ * @param $email The email address of the attendee.
+ * @param $status The participant status to set.
+ * @param $fullname The full name of the participant to set.
+ */
+ function updateAttendee($email, $status, $fullname = '')
+ {
+ foreach ($this->_attributes as $key => $attribute) {
+ if ($attribute['name'] == 'ATTENDEE' &&
+ $attribute['value'] == 'mailto:' . $email) {
+ $this->_attributes[$key]['params']['PARTSTAT'] = $status;
+ if (!empty($fullname)) {
+ $this->_attributes[$key]['params']['CN'] = $fullname;
+ }
+ unset($this->_attributes[$key]['params']['RSVP']);
+ return;
+ }
+ }
+ $params = array('PARTSTAT' => $status);
+ if (!empty($fullname)) {
+ $params['CN'] = $fullname;
+ }
+ $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params);
+ }
+
+ /**
+ * Return the organizer display name or email.
+ *
+ * @return string The organizer name to display for this event.
+ */
+ function organizerName()
+ {
+ $organizer = $this->getAttribute('ORGANIZER', true);
+ if (is_a($organizer, 'PEAR_Error')) {
+ return _("An unknown person");
+ }
+
+ if (isset($organizer[0]['CN'])) {
+ return $organizer[0]['CN'];
+ }
+
+ $organizer = parse_url($this->getAttribute('ORGANIZER'));
+
+ return $organizer['path'];
+ }
+
+ /**
+ * Update this event with details from another event.
+ *
+ * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details.
+ */
+ function updateFromvEvent($vevent)
+ {
+ $newAttributes = $vevent->getAllAttributes();
+ foreach ($newAttributes as $newAttribute) {
+ $currentValue = $this->getAttribute($newAttribute['name']);
+ if (is_a($currentValue, 'PEAR_error')) {
+ // Already exists so just add it.
+ $this->setAttribute($newAttribute['name'],
+ $newAttribute['value'],
+ $newAttribute['params']);
+ } else {
+ // Already exists so locate and modify.
+ $found = false;
+
+ // Try matching the attribte name and value incase
+ // only the params changed (eg attendee updating
+ // status).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name'] &&
+ $attr['value'] == $newAttribute['value']) {
+ // merge the params
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ // Else match the first attribute with the same
+ // name (eg changing start time).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name']) {
+ $this->_attributes[$id]['value'] = $newAttribute['value'];
+ // Merge the params.
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Update just the attendess of event with details from another
+ * event.
+ *
+ * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details
+ */
+ function updateAttendeesFromvEvent($vevent)
+ {
+ $newAttributes = $vevent->getAllAttributes();
+ foreach ($newAttributes as $newAttribute) {
+ if ($newAttribute['name'] != 'ATTENDEE') {
+ continue;
+ }
+ $currentValue = $this->getAttribute($newAttribute['name']);
+ if (is_a($currentValue, 'PEAR_error')) {
+ // Already exists so just add it.
+ $this->setAttribute($newAttribute['name'],
+ $newAttribute['value'],
+ $newAttribute['params']);
+ } else {
+ // Already exists so locate and modify.
+ $found = false;
+ // Try matching the attribte name and value incase
+ // only the params changed (eg attendee updating
+ // status).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name'] &&
+ $attr['value'] == $newAttribute['value']) {
+ // Merge the params.
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ // Else match the first attribute with the same
+ // name (eg changing start time).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name']) {
+ $this->_attributes[$id]['value'] = $newAttribute['value'];
+ // Merge the params.
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
+
+/**
+ * Class representing vFreebusy components.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @todo Don't use timestamps
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vfreebusy extends Horde_iCalendar {
+
+ var $_busyPeriods = array();
+ var $_extraParams = array();
+
+ /**
+ * Returns the type of this calendar component.
+ *
+ * @return string The type of this component.
+ */
+ function getType()
+ {
+ return 'vFreebusy';
+ }
+
+ /**
+ * Parses a string containing vFreebusy data.
+ *
+ * @param string $data The data to parse.
+ */
+ function parsevCalendar($data, $type = null, $charset = null)
+ {
+ parent::parsevCalendar($data, 'VFREEBUSY', $charset);
+
+ // Do something with all the busy periods.
+ foreach ($this->_attributes as $key => $attribute) {
+ if ($attribute['name'] != 'FREEBUSY') {
+ continue;
+ }
+ foreach ($attribute['values'] as $value) {
+ $params = isset($attribute['params'])
+ ? $attribute['params']
+ : array();
+ if (isset($value['duration'])) {
+ $this->addBusyPeriod('BUSY', $value['start'], null,
+ $value['duration'], $params);
+ } else {
+ $this->addBusyPeriod('BUSY', $value['start'],
+ $value['end'], null, $params);
+ }
+ }
+ unset($this->_attributes[$key]);
+ }
+ }
+
+ /**
+ * Returns the component exported as string.
+ *
+ * @return string The exported vFreeBusy information according to the
+ * iCalender format specification.
+ */
+ function exportvCalendar()
+ {
+ foreach ($this->_busyPeriods as $start => $end) {
+ $periods = array(array('start' => $start, 'end' => $end));
+ $this->setAttribute('FREEBUSY', $periods,
+ isset($this->_extraParams[$start])
+ ? $this->_extraParams[$start] : array());
+ }
+
+ $res = parent::_exportvData('VFREEBUSY');
+
+ foreach ($this->_attributes as $key => $attribute) {
+ if ($attribute['name'] == 'FREEBUSY') {
+ unset($this->_attributes[$key]);
+ }
+ }
+
+ return $res;
+ }
+
+ /**
+ * Returns a display name for this object.
+ *
+ * @return string A clear text name for displaying this object.
+ */
+ function getName()
+ {
+ $name = '';
+ $method = !empty($this->_container) ?
+ $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+ if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+ $attr = 'ORGANIZER';
+ } elseif ($method == 'REPLY') {
+ $attr = 'ATTENDEE';
+ }
+
+ $name = $this->getAttribute($attr, true);
+ if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) {
+ return $name[0]['CN'];
+ }
+
+ $name = $this->getAttribute($attr);
+ if (is_a($name, 'PEAR_Error')) {
+ return '';
+ } else {
+ $name = parse_url($name);
+ return $name['path'];
+ }
+ }
+
+ /**
+ * Returns the email address for this object.
+ *
+ * @return string The email address of this object's owner.
+ */
+ function getEmail()
+ {
+ $name = '';
+ $method = !empty($this->_container)
+ ? $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+ if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+ $attr = 'ORGANIZER';
+ } elseif ($method == 'REPLY') {
+ $attr = 'ATTENDEE';
+ }
+
+ $name = $this->getAttribute($attr);
+ if (is_a($name, 'PEAR_Error')) {
+ return '';
+ } else {
+ $name = parse_url($name);
+ return $name['path'];
+ }
+ }
+
+ /**
+ * Returns the busy periods.
+ *
+ * @return array All busy periods.
+ */
+ function getBusyPeriods()
+ {
+ return $this->_busyPeriods;
+ }
+
+ /**
+ * Returns any additional freebusy parameters.
+ *
+ * @return array Additional parameters of the freebusy periods.
+ */
+ function getExtraParams()
+ {
+ return $this->_extraParams;
+ }
+
+ /**
+ * Returns all the free periods of time in a given period.
+ *
+ * @param integer $startStamp The start timestamp.
+ * @param integer $endStamp The end timestamp.
+ *
+ * @return array A hash with free time periods, the start times as the
+ * keys and the end times as the values.
+ */
+ function getFreePeriods($startStamp, $endStamp)
+ {
+ $this->simplify();
+ $periods = array();
+
+ // Check that we have data for some part of this period.
+ if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
+ return $periods;
+ }
+
+ // Locate the first time in the requested period we have data for.
+ $nextstart = max($startStamp, $this->getStart());
+
+ // Check each busy period and add free periods in between.
+ foreach ($this->_busyPeriods as $start => $end) {
+ if ($start <= $endStamp && $end >= $nextstart) {
+ if ($nextstart <= $start) {
+ $periods[$nextstart] = min($start, $endStamp);
+ }
+ $nextstart = min($end, $endStamp);
+ }
+ }
+
+ // If we didn't read the end of the requested period but still have
+ // data then mark as free to the end of the period or available data.
+ if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
+ $periods[$nextstart] = min($this->getEnd(), $endStamp);
+ }
+
+ return $periods;
+ }
+
+ /**
+ * Adds a busy period to the info.
+ *
+ * This function may throw away data in case you add a period with a start
+ * date that already exists. The longer of the two periods will be chosen
+ * (and all information associated with the shorter one will be removed).
+ *
+ * @param string $type The type of the period. Either 'FREE' or
+ * 'BUSY'; only 'BUSY' supported at the moment.
+ * @param integer $start The start timestamp of the period.
+ * @param integer $end The end timestamp of the period.
+ * @param integer $duration The duration of the period. If specified, the
+ * $end parameter will be ignored.
+ * @param array $extra Additional parameters for this busy period.
+ */
+ function addBusyPeriod($type, $start, $end = null, $duration = null,
+ $extra = array())
+ {
+ if ($type == 'FREE') {
+ // Make sure this period is not marked as busy.
+ return false;
+ }
+
+ // Calculate the end time if duration was specified.
+ $tempEnd = is_null($duration) ? $end : $start + $duration;
+
+ // Make sure the period length is always positive.
+ $end = max($start, $tempEnd);
+ $start = min($start, $tempEnd);
+
+ if (isset($this->_busyPeriods[$start])) {
+ // Already a period starting at this time. Change the current
+ // period only if the new one is longer. This might be a problem
+ // if the callee assumes that there is no simplification going
+ // on. But since the periods are stored using the start time of
+ // the busy periods we have to throw away data here.
+ if ($end > $this->_busyPeriods[$start]) {
+ $this->_busyPeriods[$start] = $end;
+ $this->_extraParams[$start] = $extra;
+ }
+ } else {
+ // Add a new busy period.
+ $this->_busyPeriods[$start] = $end;
+ $this->_extraParams[$start] = $extra;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the timestamp of the start of the time period this free busy
+ * information covers.
+ *
+ * @return integer A timestamp.
+ */
+ function getStart()
+ {
+ if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) {
+ return $this->getAttribute('DTSTART');
+ } elseif (count($this->_busyPeriods)) {
+ return min(array_keys($this->_busyPeriods));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the timestamp of the end of the time period this free busy
+ * information covers.
+ *
+ * @return integer A timestamp.
+ */
+ function getEnd()
+ {
+ if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) {
+ return $this->getAttribute('DTEND');
+ } elseif (count($this->_busyPeriods)) {
+ return max(array_values($this->_busyPeriods));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Merges the busy periods of another Horde_iCalendar_vfreebusy object
+ * into this one.
+ *
+ * This might lead to simplification no matter what you specify for the
+ * "simplify" flag since periods with the same start date will lead to the
+ * shorter period being removed (see addBusyPeriod).
+ *
+ * @param Horde_iCalendar_vfreebusy $freebusy A freebusy object.
+ * @param boolean $simplify If true, simplify() will
+ * called after the merge.
+ */
+ function merge($freebusy, $simplify = true)
+ {
+ if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) {
+ return false;
+ }
+
+ $extra = $freebusy->getExtraParams();
+ foreach ($freebusy->getBusyPeriods() as $start => $end) {
+ // This might simplify the busy periods without taking the
+ // "simplify" flag into account.
+ $this->addBusyPeriod('BUSY', $start, $end, null,
+ isset($extra[$start])
+ ? $extra[$start] : array());
+ }
+
+ $thisattr = $this->getAttribute('DTSTART');
+ $thatattr = $freebusy->getAttribute('DTSTART');
+ if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+ $this->setAttribute('DTSTART', $thatattr, array(), false);
+ } elseif (!is_a($thatattr, 'PEAR_Error')) {
+ if ($thatattr < $thisattr) {
+ $this->setAttribute('DTSTART', $thatattr, array(), false);
+ }
+ }
+
+ $thisattr = $this->getAttribute('DTEND');
+ $thatattr = $freebusy->getAttribute('DTEND');
+ if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+ $this->setAttribute('DTEND', $thatattr, array(), false);
+ } elseif (!is_a($thatattr, 'PEAR_Error')) {
+ if ($thatattr > $thisattr) {
+ $this->setAttribute('DTEND', $thatattr, array(), false);
+ }
+ }
+
+ if ($simplify) {
+ $this->simplify();
+ }
+
+ return true;
+ }
+
+ /**
+ * Removes all overlaps and simplifies the busy periods array as much as
+ * possible.
+ */
+ function simplify()
+ {
+ $clean = false;
+ $busy = array($this->_busyPeriods, $this->_extraParams);
+ while (!$clean) {
+ $result = $this->_simplify($busy[0], $busy[1]);
+ $clean = $result === $busy;
+ $busy = $result;
+ }
+
+ ksort($result[1], SORT_NUMERIC);
+ $this->_extraParams = $result[1];
+
+ ksort($result[0], SORT_NUMERIC);
+ $this->_busyPeriods = $result[0];
+ }
+
+ function _simplify($busyPeriods, $extraParams = array())
+ {
+ $checked = array();
+ $checkedExtra = array();
+ $checkedEmpty = true;
+
+ foreach ($busyPeriods as $start => $end) {
+ if ($checkedEmpty) {
+ $checked[$start] = $end;
+ $checkedExtra[$start] = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ $checkedEmpty = false;
+ } else {
+ $added = false;
+ foreach ($checked as $testStart => $testEnd) {
+ // Replace old period if the new period lies around the
+ // old period.
+ if ($start <= $testStart && $end >= $testEnd) {
+ // Remove old period entry.
+ unset($checked[$testStart]);
+ unset($checkedExtra[$testStart]);
+ // Add replacing entry.
+ $checked[$start] = $end;
+ $checkedExtra[$start] = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ $added = true;
+ } elseif ($start >= $testStart && $end <= $testEnd) {
+ // The new period lies fully within the old
+ // period. Just forget about it.
+ $added = true;
+ } elseif (($end <= $testEnd && $end >= $testStart) ||
+ ($start >= $testStart && $start <= $testEnd)) {
+ // Now we are in trouble: Overlapping time periods. If
+ // we allow for additional parameters we cannot simply
+ // choose one of the two parameter sets. It's better
+ // to leave two separated time periods.
+ $extra = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ $testExtra = isset($checkedExtra[$testStart])
+ ? $checkedExtra[$testStart] : array();
+ // Remove old period entry.
+ unset($checked[$testStart]);
+ unset($checkedExtra[$testStart]);
+ // We have two periods overlapping. Are their
+ // additional parameters the same or different?
+ $newStart = min($start, $testStart);
+ $newEnd = max($end, $testEnd);
+ if ($extra === $testExtra) {
+ // Both periods have the same information. So we
+ // can just merge.
+ $checked[$newStart] = $newEnd;
+ $checkedExtra[$newStart] = $extra;
+ } else {
+ // Extra parameters are different. Create one
+ // period at the beginning with the params of the
+ // first period and create a trailing period with
+ // the params of the second period. The break
+ // point will be the end of the first period.
+ $break = min($end, $testEnd);
+ $checked[$newStart] = $break;
+ $checkedExtra[$newStart] =
+ isset($extraParams[$newStart])
+ ? $extraParams[$newStart] : array();
+ $checked[$break] = $newEnd;
+ $highStart = max($start, $testStart);
+ $checkedExtra[$break] =
+ isset($extraParams[$highStart])
+ ? $extraParams[$highStart] : array();
+
+ // Ensure we also have the extra data in the
+ // extraParams.
+ $extraParams[$break] =
+ isset($extraParams[$highStart])
+ ? $extraParams[$highStart] : array();
+ }
+ $added = true;
+ }
+
+ if ($added) {
+ break;
+ }
+ }
+
+ if (!$added) {
+ $checked[$start] = $end;
+ $checkedExtra[$start] = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ }
+ }
+ }
+
+ return array($checked, $checkedExtra);
+ }
+
+}
+
+/**
+ * Class representing vJournals.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vjournal extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vJournal';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VJOURNAL');
+ }
+
+}
+
+
+
+
+/**
+ * Class representing vNotes.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @author Karsten Fourmont <fourmont at gmx.de>
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vnote extends Horde_iCalendar {
+
+ function Horde_iCalendar_vnote($version = '1.1')
+ {
+ return parent::Horde_iCalendar($version);
+ }
+
+ function getType()
+ {
+ return 'vNote';
+ }
+
+ /**
+ * Unlike vevent and vtodo, a vnote is normally not enclosed in an
+ * iCalendar container. (BEGIN..END)
+ */
+ function exportvCalendar()
+ {
+ $requiredAttributes['BODY'] = '';
+ $requiredAttributes['VERSION'] = '1.1';
+
+ foreach ($requiredAttributes as $name => $default_value) {
+ if (is_a($this->getattribute($name), 'PEAR_Error')) {
+ $this->setAttribute($name, $default_value);
+ }
+ }
+
+ return $this->_exportvData('VNOTE');
+ }
+
+}
+
+/**
+ * Class representing vTimezones.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtimezone extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vTimeZone';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VTIMEZONE');
+ }
+
+ /**
+ * Parse child components of the vTimezone component. Returns an
+ * array with the exact time of the time change as well as the
+ * 'from' and 'to' offsets around the change. Time is arbitrarily
+ * based on UTC for comparison.
+ */
+ function parseChild(&$child, $year)
+ {
+ // Make sure 'time' key is first for sort().
+ $result['time'] = 0;
+
+ $t = $child->getAttribute('TZOFFSETFROM');
+ if (is_a($t, 'PEAR_Error')) {
+ return false;
+ }
+ $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+ $t = $child->getAttribute('TZOFFSETTO');
+ if (is_a($t, 'PEAR_Error')) {
+ return false;
+ }
+ $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+ $switch_time = $child->getAttribute('DTSTART');
+ if (is_a($switch_time, 'PEAR_Error')) {
+ return false;
+ }
+
+ $rrules = $child->getAttribute('RRULE');
+ if (is_a($rrules, 'PEAR_Error')) {
+ if (!is_int($switch_time)) {
+ return false;
+ }
+ // Convert this timestamp from local time to UTC for
+ // comparison (All dates are compared as if they are UTC).
+ $t = getdate($switch_time);
+ $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'],
+ $t['mon'], $t['mday'], $t['year']);
+ return $result;
+ }
+
+ $rrules = explode(';', $rrules);
+ foreach ($rrules as $rrule) {
+ $t = explode('=', $rrule);
+ switch ($t[0]) {
+ case 'FREQ':
+ if ($t[1] != 'YEARLY') {
+ return false;
+ }
+ break;
+
+ case 'INTERVAL':
+ if ($t[1] != '1') {
+ return false;
+ }
+ break;
+
+ case 'BYMONTH':
+ $month = intval($t[1]);
+ break;
+
+ case 'BYDAY':
+ $len = strspn($t[1], '1234567890-+');
+ if ($len == 0) {
+ return false;
+ }
+ $weekday = substr($t[1], $len);
+ $weekdays = array(
+ 'SU' => 0,
+ 'MO' => 1,
+ 'TU' => 2,
+ 'WE' => 3,
+ 'TH' => 4,
+ 'FR' => 5,
+ 'SA' => 6
+ );
+ $weekday = $weekdays[$weekday];
+ $which = intval(substr($t[1], 0, $len));
+ break;
+
+ case 'UNTIL':
+ if (intval($year) > intval(substr($t[1], 0, 4))) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (empty($month) || !isset($weekday)) {
+ return false;
+ }
+
+ if (is_int($switch_time)) {
+ // Was stored as localtime.
+ $switch_time = strftime('%H:%M:%S', $switch_time);
+ $switch_time = explode(':', $switch_time);
+ } else {
+ $switch_time = explode('T', $switch_time);
+ if (count($switch_time) != 2) {
+ return false;
+ }
+ $switch_time[0] = substr($switch_time[1], 0, 2);
+ $switch_time[2] = substr($switch_time[1], 4, 2);
+ $switch_time[1] = substr($switch_time[1], 2, 2);
+ }
+
+ // Get the timestamp for the first day of $month.
+ $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2],
+ $month, 1, $year);
+ // Get the day of the week for the first day of $month.
+ $first_of_month_weekday = intval(gmstrftime('%w', $when));
+
+ // Go to the first $weekday before first day of $month.
+ if ($weekday >= $first_of_month_weekday) {
+ $weekday -= 7;
+ }
+ $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24;
+
+ // If going backwards go to the first $weekday after last day
+ // of $month.
+ if ($which < 0) {
+ do {
+ $when += 60*60*24*7;
+ } while (intval(gmstrftime('%m', $when)) == $month);
+ }
+
+ // Calculate $weekday number $which.
+ $when += $which * 60 * 60 * 24 * 7;
+
+ $result['time'] = $when;
+
+ return $result;
+ }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_standard extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'standard';
+ }
+
+ function parsevCalendar($data)
+ {
+ parent::parsevCalendar($data, 'STANDARD');
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('STANDARD');
+ }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_daylight extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'daylight';
+ }
+
+ function parsevCalendar($data)
+ {
+ parent::parsevCalendar($data, 'DAYLIGHT');
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('DAYLIGHT');
+ }
+
+}
+
+/**
+ * Class representing vTodos.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike at graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtodo extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vTodo';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VTODO');
+ }
+
+ /**
+ * Convert this todo to an array of attributes.
+ *
+ * @return array Array containing the details of the todo in a hash
+ * as used by Horde applications.
+ */
+ function toArray()
+ {
+ $todo = array();
+
+ $name = $this->getAttribute('SUMMARY');
+ if (!is_array($name) && !is_a($name, 'PEAR_Error')) {
+ $todo['name'] = $name;
+ }
+ $desc = $this->getAttribute('DESCRIPTION');
+ if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) {
+ $todo['desc'] = $desc;
+ }
+
+ $priority = $this->getAttribute('PRIORITY');
+ if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) {
+ $todo['priority'] = $priority;
+ }
+
+ $due = $this->getAttribute('DTSTAMP');
+ if (!is_array($due) && !is_a($due, 'PEAR_Error')) {
+ $todo['due'] = $due;
+ }
+
+ return $todo;
+ }
+
+ /**
+ * Set the attributes for this todo item from an array.
+ *
+ * @param array $todo Array containing the details of the todo in
+ * the same format that toArray() exports.
+ */
+ function fromArray($todo)
+ {
+ if (isset($todo['name'])) {
+ $this->setAttribute('SUMMARY', $todo['name']);
+ }
+ if (isset($todo['desc'])) {
+ $this->setAttribute('DESCRIPTION', $todo['desc']);
+ }
+
+ if (isset($todo['priority'])) {
+ $this->setAttribute('PRIORITY', $todo['priority']);
+ }
+
+ if (isset($todo['due'])) {
+ $this->setAttribute('DTSTAMP', $todo['due']);
+ }
+ }
+
+}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 5185f17..4beef89 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -7,7 +7,7 @@
* - alarms display and dismissal
* - attachment handling
* - recurrence computation and UI elements (TODO)
- * - ical parsing and exporting (TODO)
+ * - ical parsing and exporting
*
* @version @package_version@
* @author Thomas Bruederli <bruederli at kolabsys.com>
@@ -97,7 +97,16 @@ class libcalendaring extends rcube_plugin
}
}
-
+ /**
+ * Load iCalendar functions
+ */
+ public static function get_ical()
+ {
+ $self = self::get_instance();
+ require_once($self->home . '/libvcalendar.php');
+ return new libvcalendar($self->timezone);
+ }
+
/**
* Shift dates into user's current timezone
*
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
new file mode 100644
index 0000000..be6f93d
--- /dev/null
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -0,0 +1,575 @@
+<?php
+
+/**
+ * iCalendar functions for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Lazlo Westerhof <hello at lazlo.me>
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ * @author Bogomil "Bogo" Shopov <shopov at kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello at lazlo.me>
+ * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+/**
+ * Class to parse and build vCalendar (iCalendar) files
+ *
+ * Uses the Horde:iCalendar class for parsing. To install:
+ * > pear channel-discover pear.horde.org
+ * > pear install horde/Horde_Icalendar
+ *
+ */
+class libvcalendar
+{
+ const EOL = "\r\n";
+
+ private $timezone;
+
+ public $method;
+ public $events = array();
+
+ function __construct($timezone = null)
+ {
+ $this->timezone = $timezone ? $timezone : new DateTimezone('UTC');
+ }
+
+ /**
+ * Import events from iCalendar format
+ *
+ * @param string vCalendar input
+ * @param string Input charset (from envelope)
+ * @return array List of events extracted from the input
+ */
+ public function import($vcal, $charset = RCMAIL_CHARSET)
+ {
+ $parser = $this->get_parser();
+ $parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
+ $this->method = $parser->getAttributeDefault('METHOD', '');
+ $this->events = $seen = array();
+ if ($data = $parser->getComponents()) {
+ foreach ($data as $comp) {
+ if ($comp->getType() == 'vEvent') {
+ $event = $this->_to_rcube_format($comp);
+ if (!$seen[$event['uid']]++)
+ $this->events[] = $event;
+ }
+ }
+ }
+
+ return $this->events;
+ }
+
+ /**
+ * Read iCalendar events from a file
+ *
+ * @param string File path to read from
+ * @return array List of events extracted from the file
+ */
+ public function import_from_file($filepath)
+ {
+ $this->events = $seen = array();
+ $fp = fopen($filepath, 'r');
+
+ // check file content first
+ $begin = fread($fp, 1024);
+ if (!preg_match('/BEGIN:VCALENDAR/i', $begin))
+ return $this->events;
+
+ $parser = $this->get_parser();
+ $buffer = '';
+
+ fseek($fp, 0);
+ while (($line = fgets($fp, 2048)) !== false) {
+ $buffer .= $line;
+ if (preg_match('/END:VEVENT/i', $line)) {
+ if (preg_match('/BEGIN:VCALENDAR/i', $buffer))
+ $buffer .= self::EOL ."END:VCALENDAR";
+ $parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false);
+ $buffer = '';
+ }
+ }
+ fclose($fp);
+
+ if ($data = $parser->getComponents()) {
+ foreach ($data as $comp) {
+ if ($comp->getType() == 'vEvent') {
+ $event = $this->_to_rcube_format($comp);
+ if (!$seen[$event['uid']]++)
+ $this->events[] = $event;
+ }
+ }
+ }
+
+ $this->method = $parser->getAttributeDefault('METHOD', '');
+
+ return $this->events;
+ }
+
+ /**
+ * Load iCal parser from the Horde lib
+ */
+ public function get_parser()
+ {
+ if (!class_exists('Horde_iCalendar'))
+ require_once(__DIR__ . '/lib/Horde_iCalendar.php');
+
+ // set target charset for parsed events
+ $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
+
+ return new Horde_iCalendar;
+ }
+
+ /**
+ * Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
+ */
+ private function _to_rcube_format($ve)
+ {
+ $event = array(
+ 'uid' => $ve->getAttributeDefault('UID'),
+ 'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
+ 'title' => $ve->getAttributeDefault('SUMMARY'),
+ 'start' => $this->_date2time($ve->getAttribute('DTSTART')),
+ 'end' => $this->_date2time($ve->getAttribute('DTEND')),
+ // set defaults
+ 'free_busy' => 'busy',
+ 'priority' => 0,
+ 'attendees' => array(),
+ );
+
+ // check for all-day dates
+ if (is_array($ve->getAttribute('DTSTART')))
+ $event['allday'] = true;
+
+ if ($event['allday'])
+ $event['end']->sub(new DateInterval('PT23H'));
+
+ // assign current timezone to event start/end
+ if (is_a($event['start'], 'DateTime'))
+ $event['start']->setTimezone($this->timezone);
+ else
+ unset($event['start']);
+
+ if (is_a($event['end'], 'DateTime'))
+ $event['end']->setTimezone($this->timezone);
+ else
+ unset($event['end']);
+
+ // map other attributes to internal fields
+ $_attendees = array();
+ foreach ($ve->getAllAttributes() as $attr) {
+ switch ($attr['name']) {
+ case 'ORGANIZER':
+ $organizer = array(
+ 'name' => $attr['params']['CN'],
+ 'email' => preg_replace('/^mailto:/i', '', $attr['value']),
+ 'role' => 'ORGANIZER',
+ 'status' => 'ACCEPTED',
+ );
+ if (isset($_attendees[$organizer['email']])) {
+ $i = $_attendees[$organizer['email']];
+ $event['attendees'][$i]['role'] = $organizer['role'];
+ }
+ break;
+
+ case 'ATTENDEE':
+ $attendee = array(
+ 'name' => $attr['params']['CN'],
+ 'email' => preg_replace('/^mailto:/i', '', $attr['value']),
+ 'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
+ 'status' => $attr['params']['PARTSTAT'],
+ 'rsvp' => $attr['params']['RSVP'] == 'TRUE',
+ );
+ if ($organizer && $organizer['email'] == $attendee['email'])
+ $attendee['role'] = 'ORGANIZER';
+
+ $event['attendees'][] = $attendee;
+ $_attendees[$attendee['email']] = count($event['attendees']) - 1;
+ break;
+
+ case 'TRANSP':
+ $event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
+ break;
+
+ case 'STATUS':
+ if ($attr['value'] == 'TENTATIVE')
+ $event['free_busy'] = 'tentative';
+ else if ($attr['value'] == 'CANCELLED')
+ $event['cancelled'] = true;
+ break;
+
+ case 'PRIORITY':
+ if (is_numeric($attr['value'])) {
+ $event['priority'] = $attr['value'];
+ }
+ break;
+
+ case 'RRULE':
+ // parse recurrence rule attributes
+ foreach (explode(';', $attr['value']) as $par) {
+ list($k, $v) = explode('=', $par);
+ $params[$k] = $v;
+ }
+ if ($params['UNTIL'])
+ $params['UNTIL'] = date_create($params['UNTIL']);
+ if (!$params['INTERVAL'])
+ $params['INTERVAL'] = 1;
+
+ $event['recurrence'] = $params;
+ break;
+
+ case 'EXDATE':
+ $event['recurrence']['EXDATE'][] = $this->_date2time($attr['value']);
+ break;
+
+ case 'RECURRENCE-ID':
+ $event['recurrence_id'] = $this->_date2time($attr['value']);
+ break;
+
+ case 'SEQUENCE':
+ $event['sequence'] = intval($attr['value']);
+ break;
+
+ case 'CATEGORIES':
+ case 'DESCRIPTION':
+ case 'LOCATION':
+ case 'URL':
+ $event[strtolower($attr['name'])] = $attr['value'];
+ break;
+
+ case 'CLASS':
+ case 'X-CALENDARSERVER-ACCESS':
+ $sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
+ $event['sensitivity'] = $sensitivity_map[$attr['value']];
+ break;
+
+ case 'X-MICROSOFT-CDO-BUSYSTATUS':
+ if ($attr['value'] == 'OOF')
+ $event['free_busy'] == 'outofoffice';
+ else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
+ $event['free_busy'] = strtolower($attr['value']);
+ break;
+
+ case 'ATTACH':
+ // decode inline attachment
+ if (strtoupper($attr['params']['VALUE']) == 'BINARY' && !empty($attr['value'])) {
+ $data = !strcasecmp($attr['params']['ENCODING'], 'BASE64') ? base64_decode($attr['value']) : $attr['value'];
+ $mimetype = $attr['params']['FMTTYPE'] ? $attr['params']['FMTTYPE'] : rcube_mime::file_content_type($data, $attr['params']['X-LABEL'], 'application/octet-stream', true);
+ $extensions = rcube_mime::get_mime_extensions($mimetype);
+ $filename = $attr['params']['X-LABEL'] ? $attr['params']['X-LABEL'] : 'attachment' . count($event['attachments']) . '.' . $extensions[0];
+ $event['attachments'][] = array(
+ 'mimetype' => $mimetype,
+ 'name' => $filename,
+ 'data' => $data,
+ 'size' => strlen($data),
+ );
+ }
+ else if (!empty($attr['value']) && preg_match('!^[hftps]+://!', $attr['value'])) {
+ // TODO: add support for displaying/managing link attachments in UI
+ $event['links'][] = $attr['value'];
+ }
+ break;
+
+ default:
+ if (substr($attr['name'], 0, 2) == 'X-')
+ $event['x-custom'][] = array($attr['name'], $attr['value']);
+ }
+ }
+
+ // find alarms
+ if ($valarm = $ve->findComponent('valarm')) {
+ $action = 'DISPLAY';
+ $trigger = null;
+
+ foreach ($valarm->getAllAttributes() as $attr) {
+ switch ($attr['name']) {
+ case 'TRIGGER':
+ if ($attr['params']['VALUE'] == 'DATE-TIME') {
+ $trigger = '@' . $attr['value'];
+ }
+ else {
+ $trigger = $attr['value'];
+ $offset = abs($trigger);
+ $unit = 'S';
+ if ($offset % 86400 == 0) {
+ $unit = 'D';
+ $trigger = intval($trigger / 86400);
+ }
+ else if ($offset % 3600 == 0) {
+ $unit = 'H';
+ $trigger = intval($trigger / 3600);
+ }
+ else if ($offset % 60 == 0) {
+ $unit = 'M';
+ $trigger = intval($trigger / 60);
+ }
+ }
+ break;
+
+ case 'ACTION':
+ $action = $attr['value'];
+ break;
+ }
+ }
+ if ($trigger)
+ $event['alarms'] = $trigger . $unit . ':' . $action;
+ }
+
+ // add organizer to attendees list if not already present
+ if ($organizer && !isset($_attendees[$organizer['email']]))
+ array_unshift($event['attendees'], $organizer);
+
+ // make sure the event has an UID
+# if (!$event['uid'])
+# $event['uid'] = $this->cal->generate_uid();
+
+ return $event;
+ }
+
+ /**
+ * Helper method to correctly interpret an all-day date value
+ */
+ private function _date2time($prop)
+ {
+ // create timestamp at 12:00 in user's timezone
+ if (is_array($prop))
+ return date_create(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->timezone);
+ else if (is_numeric($prop))
+ return date_create('@'.$prop);
+
+ return $prop;
+ }
+
+
+ /**
+ * Free resources by clearing member vars
+ */
+ public function reset()
+ {
+ $this->method = '';
+ $this->events = array();
+ }
+
+ /**
+ * Export events to iCalendar format
+ *
+ * @param array Events as array
+ * @param string VCalendar method to advertise
+ * @param boolean Directly send data to stdout instead of returning
+ * @param callable Callback function to fetch attachment contents, false if no attachment export
+ * @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
+ */
+ public function export($events, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
+ {
+ $memory_limit = parse_bytes(ini_get('memory_limit'));
+
+ if (!$recurrence_id) {
+ $ical = "BEGIN:VCALENDAR" . self::EOL;
+ $ical .= "VERSION:2.0" . self::EOL;
+ $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
+ $ical .= "CALSCALE:GREGORIAN" . self::EOL;
+
+ if ($method)
+ $ical .= "METHOD:" . strtoupper($method) . self::EOL;
+
+ if ($write) {
+ echo $ical;
+ $ical = '';
+ }
+ }
+
+ foreach ($events as $event) {
+ $vevent = "BEGIN:VEVENT" . self::EOL;
+ $vevent .= "UID:" . self::escape($event['uid']) . self::EOL;
+ $vevent .= $this->format_datetime("DTSTAMP", $event['changed'] ?: new DateTime(), false, true) . self::EOL;
+ if ($event['sequence'])
+ $vevent .= "SEQUENCE:" . intval($event['sequence']) . self::EOL;
+ if ($recurrence_id)
+ $vevent .= $recurrence_id . self::EOL;
+
+ // correctly set all-day dates
+ if ($event['allday']) {
+ $event['end'] = clone $event['end'];
+ $event['end']->add(new DateInterval('P1D')); // ends the next day
+ $vevent .= $this->format_datetime("DTSTART", $event['start'], true) . self::EOL;
+ $vevent .= $this->format_datetime("DTEND", $event['end'], true) . self::EOL;
+ }
+ else {
+ $vevent .= $this->format_datetime("DTSTART", $event['start'], false) . self::EOL;
+ $vevent .= $this->format_datetime("DTEND", $event['end'], false) . self::EOL;
+ }
+ $vevent .= "SUMMARY:" . self::escape($event['title']) . self::EOL;
+ $vevent .= "DESCRIPTION:" . self::escape($event['description']) . self::EOL;
+
+ if (!empty($event['attendees'])){
+ $vevent .= $this->_get_attendees($event['attendees']);
+ }
+
+ if (!empty($event['location'])) {
+ $vevent .= "LOCATION:" . self::escape($event['location']) . self::EOL;
+ }
+ if (!empty($event['url'])) {
+ $vevent .= "URL:" . self::escape($event['url']) . self::EOL;
+ }
+ if ($event['recurrence'] && !$recurrence_id) {
+ $vevent .= "RRULE:" . libcalendaring::to_rrule($event['recurrence'], self::EOL) . self::EOL;
+
+ foreach ((array)$event['recurrence']['EXDATE'] as $ex) {
+ $vevent .= $this->format_datetime("EXDATE", $ex, false, true) . self::EOL;
+ }
+ }
+ if (!empty($event['categories'])) {
+ $vevent .= "CATEGORIES:" . self::escape(strtoupper($event['categories'])) . self::EOL;
+ }
+ if ($event['sensitivity'] > 0) {
+ $vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL;
+ }
+ if ($event['alarms']) {
+ list($trigger, $action) = explode(':', $event['alarms']);
+ $val = libcalendaring::parse_alaram_value($trigger);
+
+ $vevent .= "BEGIN:VALARM\n";
+ if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
+ else $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
+ if ($action) $vevent .= "ACTION:" . self::escape(strtoupper($action)) . self::EOL;
+ $vevent .= "END:VALARM\n";
+ }
+
+ $vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
+
+ if ($event['priority']) {
+ $vevent .= "PRIORITY:" . $event['priority'] . self::EOL;
+ }
+
+ if ($event['cancelled'])
+ $vevent .= "STATUS:CANCELLED" . self::EOL;
+ else if ($event['free_busy'] == 'tentative')
+ $vevent .= "STATUS:TENTATIVE" . self::EOL;
+
+ foreach ((array)$event['x-custom'] as $prop)
+ $vevent .= $prop[0] . ':' . self::escape($prop[1]) . self::EOL;
+
+ // export attachments using the given callback function
+ if (is_callable($get_attachment) && !empty($event['attachments'])) {
+ foreach ((array)$event['attachments'] as $attach) {
+ // check available memory and skip attachment export if we can't buffer it
+ if ($memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
+ && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
+ continue;
+ }
+ // TODO: let the callback print the data directly to stdout (with b64 encoding)
+ if ($data = call_user_func($get_attachment, $attach['id'], $event)) {
+ $vevent .= sprintf('ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=%s;X-LABEL=%s:',
+ self::escape($attach['mimetype']), self::escape($attach['name']));
+ $vevent .= base64_encode($data) . self::EOL;
+ }
+ unset($data); // attempt to free memory
+ }
+ }
+
+ $vevent .= "END:VEVENT" . self::EOL;
+
+ // append recurrence exceptions
+ if ($event['recurrence']['EXCEPTIONS'] && !$recurrence_id) {
+ foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
+ $exdate = clone $event['start'];
+ $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
+ $vevent .= $this->export(array($ex), null, false, $get_attachment,
+ $this->format_datetime('RECURRENCE-ID', $exdate, $event['allday']));
+ }
+ }
+
+ if ($write)
+ echo rcube_vcard::rfc2425_fold($vevent);
+ else
+ $ical .= $vevent;
+ }
+
+ if (!$recurrence_id) {
+ $ical .= "END:VCALENDAR" . self::EOL;
+
+ if ($write) {
+ echo $ical;
+ return true;
+ }
+ }
+
+ // fold lines to 75 chars
+ return rcube_vcard::rfc2425_fold($ical);
+ }
+
+ private function format_datetime($attr, $dt, $dateonly = false, $utc = false)
+ {
+ if (is_numeric($dt))
+ $dt = new DateTime('@'.$dt);
+
+ if ($utc)
+ $dt->setTimezone(new DateTimeZone('UTC'));
+
+ if ($dateonly) {
+ return $attr . ';VALUE=DATE:' . $dt->format('Ymd');
+ }
+ else {
+ // <ATTR>;TZID=Europe/Zurich:20120706T210000
+ $tz = $dt->getTimezone();
+ $tzname = $tz ? $tz->getName() : null;
+ $tzid = $tzname && $tzname != 'UTC' && $tzname != '+00:00' ? ';TZID=' . self::escape($tzname) : '';
+ return $attr . $tzid . ':' . $dt->format('Ymd\THis' . ($tzid ? '' : '\Z'));
+ }
+ }
+
+ /**
+ * Escape values according to RFC 2445 4.3.11
+ */
+ private function escape($str)
+ {
+ return strtr($str, array('\\' => '\\\\', "\n" => '\n', ';' => '\;', ',' => '\,'));
+ }
+
+ /**
+ * Construct the orginizer of the event.
+ * @param Array Attendees and roles
+ *
+ */
+ private function _get_attendees($ats)
+ {
+ $organizer = "";
+ $attendees = "";
+ foreach ($ats as $at) {
+ if ($at['role'] == "ORGANIZER") {
+ if ($at['email']) {
+ $organizer .= "ORGANIZER;";
+ if (!empty($at['name']))
+ $organizer .= 'CN="' . $at['name'] . '"';
+ $organizer .= ":mailto:". $at['email'] . self::EOL;
+ }
+ }
+ else if ($at['email']) {
+ //I am an attendee
+ $attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
+ if ($at['rsvp'])
+ $attendees .= ";RSVP=TRUE";
+ if (!empty($at['name']))
+ $attendees .= ';CN="' . $at['name'] . '"';
+ $attendees .= ":mailto:" . $at['email'] . self::EOL;
+ }
+ }
+
+ return $organizer . $attendees;
+ }
+
+}
More information about the commits
mailing list