From 50569114acdc64e7c7cae1498635d3f821517c30 Mon Sep 17 00:00:00 2001 From: Daniel Lange Date: Mon, 7 Mar 2016 15:53:16 +0100 Subject: Initial commit of the Faster IT roundcube_calendar plugin distribution This includes: * Kolab plugins 3.2.9 (calendar and libcalendaring) * CalDAV driver 3.2.8 * .htaccess files for at least some security * SabreDAV updated to 1.8.12 (Jan 2015 release) * Support for CURLOPT_SSL_* settings to allow self-signed certificates * Small fixes & improved documentation --- README.md | 103 + calendar/.gitignore | 8 + calendar/README_kolab | 78 + calendar/TODO | 48 + calendar/UPGRADING | 17 + calendar/calendar.php | 3581 ++++++++++ calendar/calendar_base.js | 139 + calendar/calendar_ui.js | 4273 ++++++++++++ calendar/composer.json | 31 + calendar/config.inc.php.dist | 198 + calendar/drivers/caldav/SQL/mysql.initial.sql | 92 + calendar/drivers/caldav/SQL/mysql/.keep_dir | 0 calendar/drivers/caldav/SQL/mysql/2014081300.sql | 24 + calendar/drivers/caldav/SQL/mysql/2015022500.sql | 125 + calendar/drivers/caldav/SQL/mysql/2015022700.sql | 14 + calendar/drivers/caldav/SQL/postgres.initial.sql | 51 + calendar/drivers/caldav/caldav_driver.php | 2036 ++++++ calendar/drivers/caldav/caldav_sync.php | 253 + calendar/drivers/calendar_driver.php | 819 +++ calendar/drivers/database/SQL/mysql.initial.sql | 85 + calendar/drivers/database/SQL/mysql/2012080600.sql | 3 + calendar/drivers/database/SQL/mysql/2013011000.sql | 1 + calendar/drivers/database/SQL/mysql/2013042700.sql | 1 + calendar/drivers/database/SQL/mysql/2013051600.sql | 3 + calendar/drivers/database/SQL/mysql/2014040900.sql | 3 + calendar/drivers/database/SQL/mysql/2015022700.sql | 15 + calendar/drivers/database/SQL/postgres.initial.sql | 109 + .../drivers/database/SQL/postgres/2012080600.sql | 3 + .../drivers/database/SQL/postgres/2013011000.sql | 1 + .../drivers/database/SQL/postgres/2013042700.sql | 8 + .../drivers/database/SQL/postgres/2013051600.sql | 3 + .../drivers/database/SQL/postgres/2014040900.sql | 3 + .../drivers/database/SQL/postgres/2015022700.sql | 9 + calendar/drivers/database/SQL/sqlite.initial.sql | 79 + .../drivers/database/SQL/sqlite/2013011000.sql | 1 + .../drivers/database/SQL/sqlite/2013042700.sql | 1 + .../drivers/database/SQL/sqlite/2013051600.sql | 63 + .../drivers/database/SQL/sqlite/2014040900.sql | 67 + .../drivers/database/SQL/sqlite/2015022700.sql | 79 + calendar/drivers/database/database_driver.php | 1496 +++++ calendar/drivers/ical/SQL/mysql.initial.sql | 91 + calendar/drivers/ical/SQL/mysql/.keep_dir | 0 calendar/drivers/ical/SQL/mysql/2015022500.sql | 124 + calendar/drivers/ical/SQL/mysql/2015022700.sql | 14 + calendar/drivers/ical/ical_driver.php | 1821 ++++++ calendar/drivers/ical/ical_sync.php | 125 + calendar/drivers/kolab/SQL/mysql.initial.sql | 32 + calendar/drivers/kolab/SQL/mysql/2012080600.sql | 11 + calendar/drivers/kolab/SQL/mysql/2013011000.sql | 1 + calendar/drivers/kolab/SQL/mysql/2014041700.sql | 1 + calendar/drivers/kolab/SQL/mysql/2014082600.sql | 2 + calendar/drivers/kolab/SQL/oracle.initial.sql | 31 + calendar/drivers/kolab/SQL/postgres.initial.sql | 32 + calendar/drivers/kolab/kolab_calendar.php | 836 +++ calendar/drivers/kolab/kolab_driver.php | 2526 ++++++++ .../drivers/kolab/kolab_invitation_calendar.php | 377 ++ calendar/drivers/kolab/kolab_user_calendar.php | 432 ++ calendar/drivers/ldap/resources_driver_ldap.php | 150 + calendar/drivers/resources_driver.php | 114 + calendar/helpdocs/en_US/_static/_skin | 1 + .../helpdocs/en_US/_static/kolab/alarms-popup.png | 1 + .../helpdocs/en_US/_static/kolab/calendar-acl.png | Bin 0 -> 108292 bytes .../en_US/_static/kolab/calendar-header.png | Bin 0 -> 13036 bytes .../en_US/_static/kolab/event-participants.png | Bin 0 -> 101779 bytes .../helpdocs/en_US/_static/kolab/event-resize.png | 1 + .../en_US/_static/kolab/itip-invitation.png | 1 + .../helpdocs/en_US/_static/kolab/itip-reply.png | 1 + .../helpdocs/en_US/_static/larry/alarms-popup.png | Bin 0 -> 19181 bytes .../helpdocs/en_US/_static/larry/calendar-acl.png | Bin 0 -> 28590 bytes .../en_US/_static/larry/calendar-header.png | Bin 0 -> 23126 bytes .../en_US/_static/larry/event-participants.png | Bin 0 -> 39276 bytes .../helpdocs/en_US/_static/larry/event-resize.png | Bin 0 -> 23728 bytes .../en_US/_static/larry/itip-invitation.png | Bin 0 -> 117940 bytes .../helpdocs/en_US/_static/larry/itip-reply.png | Bin 0 -> 39276 bytes calendar/helpdocs/en_US/importexport.rst | 43 + calendar/helpdocs/en_US/index.rst | 18 + calendar/helpdocs/en_US/invitations.rst | 55 + calendar/helpdocs/en_US/manage.rst | 169 + calendar/helpdocs/en_US/overview.rst | 138 + calendar/helpdocs/en_US/settings.rst | 75 + calendar/helpdocs/en_US/sharing.rst | 49 + .../locale/bg_BG/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/bg_BG/LC_MESSAGES/index.po | 28 + .../locale/bg_BG/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/bg_BG/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/bg_BG/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/bg_BG/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/bg_BG/LC_MESSAGES/sharing.po | 99 + .../locale/ca_ES/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/ca_ES/LC_MESSAGES/index.po | 28 + .../locale/ca_ES/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/ca_ES/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/ca_ES/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/ca_ES/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/ca_ES/LC_MESSAGES/sharing.po | 99 + .../locale/cs_CZ/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/cs_CZ/LC_MESSAGES/index.po | 28 + .../locale/cs_CZ/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/cs_CZ/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/cs_CZ/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/cs_CZ/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/cs_CZ/LC_MESSAGES/sharing.po | 99 + .../locale/da_DK/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/da_DK/LC_MESSAGES/index.po | 28 + .../locale/da_DK/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/da_DK/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/da_DK/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/da_DK/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/da_DK/LC_MESSAGES/sharing.po | 99 + .../locale/de_CH/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/de_CH/LC_MESSAGES/index.po | 28 + .../locale/de_CH/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/de_CH/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/de_CH/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/de_CH/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/de_CH/LC_MESSAGES/sharing.po | 99 + .../locale/de_DE/LC_MESSAGES/importexport.po | 97 + .../helpdocs/locale/de_DE/LC_MESSAGES/index.po | 29 + .../locale/de_DE/LC_MESSAGES/invitations.po | 104 + .../helpdocs/locale/de_DE/LC_MESSAGES/manage.po | 343 + .../helpdocs/locale/de_DE/LC_MESSAGES/overview.po | 267 + .../helpdocs/locale/de_DE/LC_MESSAGES/settings.po | 179 + .../helpdocs/locale/de_DE/LC_MESSAGES/sharing.po | 100 + .../locale/en_US/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/en_US/LC_MESSAGES/index.po | 28 + .../locale/en_US/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/en_US/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/en_US/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/en_US/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/en_US/LC_MESSAGES/sharing.po | 99 + .../locale/es_AR/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/es_AR/LC_MESSAGES/index.po | 28 + .../locale/es_AR/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/es_AR/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/es_AR/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/es_AR/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/es_AR/LC_MESSAGES/sharing.po | 99 + .../locale/es_ES/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/es_ES/LC_MESSAGES/index.po | 28 + .../locale/es_ES/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/es_ES/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/es_ES/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/es_ES/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/es_ES/LC_MESSAGES/sharing.po | 99 + .../locale/et_EE/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/et_EE/LC_MESSAGES/index.po | 28 + .../locale/et_EE/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/et_EE/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/et_EE/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/et_EE/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/et_EE/LC_MESSAGES/sharing.po | 99 + .../locale/fi_FI/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/fi_FI/LC_MESSAGES/index.po | 28 + .../locale/fi_FI/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/fi_FI/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/fi_FI/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/fi_FI/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/fi_FI/LC_MESSAGES/sharing.po | 99 + .../locale/fr_FR/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/fr_FR/LC_MESSAGES/index.po | 28 + .../locale/fr_FR/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/fr_FR/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/fr_FR/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/fr_FR/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/fr_FR/LC_MESSAGES/sharing.po | 99 + .../helpdocs/locale/he/LC_MESSAGES/importexport.po | 96 + calendar/helpdocs/locale/he/LC_MESSAGES/index.po | 28 + .../helpdocs/locale/he/LC_MESSAGES/invitations.po | 103 + calendar/helpdocs/locale/he/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/he/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/he/LC_MESSAGES/settings.po | 178 + calendar/helpdocs/locale/he/LC_MESSAGES/sharing.po | 99 + .../helpdocs/locale/hr/LC_MESSAGES/importexport.po | 96 + calendar/helpdocs/locale/hr/LC_MESSAGES/index.po | 28 + .../helpdocs/locale/hr/LC_MESSAGES/invitations.po | 103 + calendar/helpdocs/locale/hr/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/hr/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/hr/LC_MESSAGES/settings.po | 178 + calendar/helpdocs/locale/hr/LC_MESSAGES/sharing.po | 99 + .../locale/hu_HU/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/hu_HU/LC_MESSAGES/index.po | 28 + .../locale/hu_HU/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/hu_HU/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/hu_HU/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/hu_HU/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/hu_HU/LC_MESSAGES/sharing.po | 99 + .../locale/it_IT/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/it_IT/LC_MESSAGES/index.po | 28 + .../locale/it_IT/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/it_IT/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/it_IT/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/it_IT/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/it_IT/LC_MESSAGES/sharing.po | 99 + .../locale/ja_JP/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/ja_JP/LC_MESSAGES/index.po | 28 + .../locale/ja_JP/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/ja_JP/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/ja_JP/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/ja_JP/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/ja_JP/LC_MESSAGES/sharing.po | 99 + .../locale/ku_IQ/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/ku_IQ/LC_MESSAGES/index.po | 28 + .../locale/ku_IQ/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/ku_IQ/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/ku_IQ/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/ku_IQ/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/ku_IQ/LC_MESSAGES/sharing.po | 99 + .../locale/nl_NL/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/nl_NL/LC_MESSAGES/index.po | 28 + .../locale/nl_NL/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/nl_NL/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/nl_NL/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/nl_NL/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/nl_NL/LC_MESSAGES/sharing.po | 99 + .../locale/pl_PL/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/pl_PL/LC_MESSAGES/index.po | 28 + .../locale/pl_PL/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/pl_PL/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/pl_PL/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/pl_PL/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/pl_PL/LC_MESSAGES/sharing.po | 99 + .../locale/pt_BR/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/pt_BR/LC_MESSAGES/index.po | 28 + .../locale/pt_BR/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/pt_BR/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/pt_BR/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/pt_BR/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/pt_BR/LC_MESSAGES/sharing.po | 99 + .../helpdocs/locale/ro/LC_MESSAGES/importexport.po | 96 + calendar/helpdocs/locale/ro/LC_MESSAGES/index.po | 28 + .../helpdocs/locale/ro/LC_MESSAGES/invitations.po | 103 + calendar/helpdocs/locale/ro/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/ro/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/ro/LC_MESSAGES/settings.po | 178 + calendar/helpdocs/locale/ro/LC_MESSAGES/sharing.po | 99 + .../locale/ru_RU/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/ru_RU/LC_MESSAGES/index.po | 28 + .../locale/ru_RU/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/ru_RU/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/ru_RU/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/ru_RU/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/ru_RU/LC_MESSAGES/sharing.po | 99 + .../helpdocs/locale/sk/LC_MESSAGES/importexport.po | 96 + calendar/helpdocs/locale/sk/LC_MESSAGES/index.po | 28 + .../helpdocs/locale/sk/LC_MESSAGES/invitations.po | 103 + calendar/helpdocs/locale/sk/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/sk/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/sk/LC_MESSAGES/settings.po | 178 + calendar/helpdocs/locale/sk/LC_MESSAGES/sharing.po | 99 + .../helpdocs/locale/sv/LC_MESSAGES/importexport.po | 96 + calendar/helpdocs/locale/sv/LC_MESSAGES/index.po | 28 + .../helpdocs/locale/sv/LC_MESSAGES/invitations.po | 103 + calendar/helpdocs/locale/sv/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/sv/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/sv/LC_MESSAGES/settings.po | 178 + calendar/helpdocs/locale/sv/LC_MESSAGES/sharing.po | 99 + .../locale/sv_SE/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/sv_SE/LC_MESSAGES/index.po | 28 + .../locale/sv_SE/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/sv_SE/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/sv_SE/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/sv_SE/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/sv_SE/LC_MESSAGES/sharing.po | 99 + .../locale/tr_TR/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/tr_TR/LC_MESSAGES/index.po | 28 + .../locale/tr_TR/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/tr_TR/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/tr_TR/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/tr_TR/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/tr_TR/LC_MESSAGES/sharing.po | 99 + .../helpdocs/locale/uk/LC_MESSAGES/importexport.po | 96 + calendar/helpdocs/locale/uk/LC_MESSAGES/index.po | 28 + .../helpdocs/locale/uk/LC_MESSAGES/invitations.po | 103 + calendar/helpdocs/locale/uk/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/uk/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/uk/LC_MESSAGES/settings.po | 178 + calendar/helpdocs/locale/uk/LC_MESSAGES/sharing.po | 99 + .../helpdocs/locale/vi/LC_MESSAGES/importexport.po | 96 + calendar/helpdocs/locale/vi/LC_MESSAGES/index.po | 28 + .../helpdocs/locale/vi/LC_MESSAGES/invitations.po | 103 + calendar/helpdocs/locale/vi/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/vi/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/vi/LC_MESSAGES/settings.po | 178 + calendar/helpdocs/locale/vi/LC_MESSAGES/sharing.po | 99 + .../locale/vi_VN/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/vi_VN/LC_MESSAGES/index.po | 28 + .../locale/vi_VN/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/vi_VN/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/vi_VN/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/vi_VN/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/vi_VN/LC_MESSAGES/sharing.po | 99 + .../locale/zh_CN/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/zh_CN/LC_MESSAGES/index.po | 28 + .../locale/zh_CN/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/zh_CN/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/zh_CN/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/zh_CN/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/zh_CN/LC_MESSAGES/sharing.po | 99 + .../locale/zh_TW/LC_MESSAGES/importexport.po | 96 + .../helpdocs/locale/zh_TW/LC_MESSAGES/index.po | 28 + .../locale/zh_TW/LC_MESSAGES/invitations.po | 103 + .../helpdocs/locale/zh_TW/LC_MESSAGES/manage.po | 342 + .../helpdocs/locale/zh_TW/LC_MESSAGES/overview.po | 266 + .../helpdocs/locale/zh_TW/LC_MESSAGES/settings.po | 178 + .../helpdocs/locale/zh_TW/LC_MESSAGES/sharing.po | 99 + calendar/helpdocs/po/importexport.pot | 86 + calendar/helpdocs/po/index.pot | 26 + calendar/helpdocs/po/invitations.pot | 74 + calendar/helpdocs/po/manage.pot | 250 + calendar/helpdocs/po/overview.pot | 195 + calendar/helpdocs/po/settings.pot | 146 + calendar/helpdocs/po/sharing.pot | 74 + calendar/lib/SabreDAV/.htaccess | 2 + calendar/lib/SabreDAV/ChangeLog | 1111 ++++ calendar/lib/SabreDAV/LICENSE | 27 + calendar/lib/SabreDAV/README.md | 30 + calendar/lib/SabreDAV/composer.json | 61 + calendar/lib/SabreDAV/composer.lock | 80 + .../OldSabre/CalDAV/Backend/AbstractBackend.php | 155 + .../OldSabre/CalDAV/Backend/BackendInterface.php | 233 + .../CalDAV/Backend/NotificationSupport.php | 47 + .../SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php | 691 ++ .../lib/OldSabre/CalDAV/Backend/SharingSupport.php | 243 + .../lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php | 376 ++ .../lib/OldSabre/CalDAV/CalendarObject.php | 279 + .../lib/OldSabre/CalDAV/CalendarQueryParser.php | 298 + .../lib/OldSabre/CalDAV/CalendarQueryValidator.php | 392 ++ .../lib/OldSabre/CalDAV/CalendarRootNode.php | 77 + .../CalDAV/Exception/InvalidComponentType.php | 35 + .../lib/OldSabre/CalDAV/ICSExportPlugin.php | 142 + .../lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php | 36 + .../lib/OldSabre/CalDAV/ICalendarObject.php | 21 + .../lib/OldSabre/CalDAV/IShareableCalendar.php | 48 + .../lib/OldSabre/CalDAV/ISharedCalendar.php | 36 + .../OldSabre/CalDAV/Notifications/Collection.php | 173 + .../OldSabre/CalDAV/Notifications/ICollection.php | 24 + .../lib/OldSabre/CalDAV/Notifications/INode.php | 38 + .../CalDAV/Notifications/INotificationType.php | 44 + .../lib/OldSabre/CalDAV/Notifications/Node.php | 192 + .../CalDAV/Notifications/Notification/Invite.php | 324 + .../Notifications/Notification/InviteReply.php | 218 + .../Notifications/Notification/SystemStatus.php | 182 + .../lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php | 1338 ++++ .../lib/OldSabre/CalDAV/Principal/Collection.php | 32 + .../lib/OldSabre/CalDAV/Principal/IProxyRead.php | 19 + .../lib/OldSabre/CalDAV/Principal/IProxyWrite.php | 19 + .../lib/OldSabre/CalDAV/Principal/ProxyRead.php | 180 + .../lib/OldSabre/CalDAV/Principal/ProxyWrite.php | 180 + .../lib/OldSabre/CalDAV/Principal/User.php | 134 + .../CalDAV/Property/AllowedSharingModes.php | 74 + .../lib/OldSabre/CalDAV/Property/Invite.php | 227 + .../CalDAV/Property/ScheduleCalendarTransp.php | 102 + .../Property/SupportedCalendarComponentSet.php | 88 + .../CalDAV/Property/SupportedCalendarData.php | 40 + .../CalDAV/Property/SupportedCollationSet.php | 45 + .../SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php | 111 + .../lib/OldSabre/CalDAV/Schedule/IOutbox.php | 16 + .../lib/OldSabre/CalDAV/Schedule/Outbox.php | 163 + .../lib/OldSabre/CalDAV/ShareableCalendar.php | 72 + .../lib/OldSabre/CalDAV/SharedCalendar.php | 116 + .../SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php | 526 ++ .../SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php | 342 + .../lib/SabreDAV/lib/OldSabre/CalDAV/Version.php | 24 + .../SabreDAV/lib/OldSabre/CardDAV/AddressBook.php | 315 + .../OldSabre/CardDAV/AddressBookQueryParser.php | 221 + .../lib/OldSabre/CardDAV/AddressBookRoot.php | 80 + .../OldSabre/CardDAV/Backend/AbstractBackend.php | 18 + .../OldSabre/CardDAV/Backend/BackendInterface.php | 166 + .../SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php | 333 + .../lib/SabreDAV/lib/OldSabre/CardDAV/Card.php | 260 + .../SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php | 20 + .../lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php | 20 + .../SabreDAV/lib/OldSabre/CardDAV/IDirectory.php | 21 + .../lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php | 706 ++ .../CardDAV/Property/SupportedAddressData.php | 72 + .../lib/OldSabre/CardDAV/UserAddressBooks.php | 260 + .../lib/OldSabre/CardDAV/VCFExportPlugin.php | 108 + .../lib/SabreDAV/lib/OldSabre/CardDAV/Version.php | 26 + .../OldSabre/DAV/Auth/Backend/AbstractBasic.php | 87 + .../OldSabre/DAV/Auth/Backend/AbstractDigest.php | 101 + .../lib/OldSabre/DAV/Auth/Backend/Apache.php | 63 + .../OldSabre/DAV/Auth/Backend/BackendInterface.php | 36 + .../lib/OldSabre/DAV/Auth/Backend/File.php | 77 + .../SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php | 65 + .../lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php | 112 + .../lib/OldSabre/DAV/Browser/GuessContentType.php | 99 + .../lib/OldSabre/DAV/Browser/MapGetToPropFind.php | 57 + .../SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php | 491 ++ .../lib/OldSabre/DAV/Browser/assets/favicon.ico | Bin 0 -> 4286 bytes .../DAV/Browser/assets/icons/addressbook.png | Bin 0 -> 7232 bytes .../OldSabre/DAV/Browser/assets/icons/calendar.png | Bin 0 -> 4388 bytes .../lib/OldSabre/DAV/Browser/assets/icons/card.png | Bin 0 -> 5695 bytes .../DAV/Browser/assets/icons/collection.png | Bin 0 -> 3474 bytes .../lib/OldSabre/DAV/Browser/assets/icons/file.png | Bin 0 -> 2837 bytes .../OldSabre/DAV/Browser/assets/icons/parent.png | Bin 0 -> 3474 bytes .../DAV/Browser/assets/icons/principal.png | Bin 0 -> 5480 bytes calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php | 578 ++ .../lib/SabreDAV/lib/OldSabre/DAV/Collection.php | 110 + .../lib/SabreDAV/lib/OldSabre/DAV/Exception.php | 64 + .../lib/OldSabre/DAV/Exception/BadRequest.php | 28 + .../lib/OldSabre/DAV/Exception/Conflict.php | 28 + .../lib/OldSabre/DAV/Exception/ConflictingLock.php | 37 + .../lib/OldSabre/DAV/Exception/FileNotFound.php | 19 + .../lib/OldSabre/DAV/Exception/Forbidden.php | 27 + .../OldSabre/DAV/Exception/InsufficientStorage.php | 27 + .../OldSabre/DAV/Exception/InvalidResourceType.php | 33 + .../lib/OldSabre/DAV/Exception/LengthRequired.php | 30 + .../DAV/Exception/LockTokenMatchesRequestUri.php | 41 + .../SabreDAV/lib/OldSabre/DAV/Exception/Locked.php | 73 + .../OldSabre/DAV/Exception/MethodNotAllowed.php | 45 + .../OldSabre/DAV/Exception/NotAuthenticated.php | 30 + .../lib/OldSabre/DAV/Exception/NotFound.php | 28 + .../lib/OldSabre/DAV/Exception/NotImplemented.php | 27 + .../lib/OldSabre/DAV/Exception/PaymentRequired.php | 30 + .../OldSabre/DAV/Exception/PreconditionFailed.php | 71 + .../OldSabre/DAV/Exception/ReportNotSupported.php | 32 + .../DAV/Exception/RequestedRangeNotSatisfiable.php | 31 + .../OldSabre/DAV/Exception/ServiceUnavailable.php | 30 + .../DAV/Exception/UnsupportedMediaType.php | 28 + .../lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php | 140 + calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php | 91 + calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php | 82 + .../SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php | 159 + .../lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php | 146 + .../lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php | 214 + calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php | 85 + .../lib/SabreDAV/lib/OldSabre/DAV/ICollection.php | 77 + .../lib/OldSabre/DAV/IExtendedCollection.php | 28 + calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php | 77 + calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php | 46 + .../lib/SabreDAV/lib/OldSabre/DAV/IProperties.php | 71 + calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php | 27 + .../OldSabre/DAV/Locks/Backend/AbstractBackend.php | 21 + .../DAV/Locks/Backend/BackendInterface.php | 51 + .../SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php | 193 + .../lib/OldSabre/DAV/Locks/Backend/File.php | 183 + .../lib/OldSabre/DAV/Locks/Backend/PDO.php | 167 + .../SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php | 81 + .../lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php | 649 ++ .../lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php | 83 + calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php | 55 + .../lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php | 159 + .../lib/OldSabre/DAV/PartialUpdate/IFile.php | 39 + .../OldSabre/DAV/PartialUpdate/IPatchSupport.php | 48 + .../lib/OldSabre/DAV/PartialUpdate/Plugin.php | 246 + .../lib/SabreDAV/lib/OldSabre/DAV/Property.php | 31 + .../lib/OldSabre/DAV/Property/GetLastModified.php | 78 + .../SabreDAV/lib/OldSabre/DAV/Property/Href.php | 99 + .../lib/OldSabre/DAV/Property/HrefList.php | 105 + .../SabreDAV/lib/OldSabre/DAV/Property/IHref.php | 25 + .../lib/OldSabre/DAV/Property/LockDiscovery.php | 104 + .../lib/OldSabre/DAV/Property/ResourceType.php | 127 + .../lib/OldSabre/DAV/Property/Response.php | 157 + .../lib/OldSabre/DAV/Property/ResponseList.php | 59 + .../lib/OldSabre/DAV/Property/SupportedLock.php | 78 + .../OldSabre/DAV/Property/SupportedReportSet.php | 111 + .../lib/OldSabre/DAV/PropertyInterface.php | 21 + calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php | 2178 +++++++ .../lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php | 90 + .../SabreDAV/lib/OldSabre/DAV/SimpleCollection.php | 108 + .../lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php | 121 + .../lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php | 91 + .../lib/OldSabre/DAV/TemporaryFileFilterPlugin.php | 289 + calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php | 193 + .../SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php | 133 + calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php | 124 + .../lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php | 64 + calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php | 24 + calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php | 191 + .../DAVACL/AbstractPrincipalCollection.php | 155 + .../lib/OldSabre/DAVACL/Exception/AceConflict.php | 35 + .../OldSabre/DAVACL/Exception/NeedPrivileges.php | 83 + .../lib/OldSabre/DAVACL/Exception/NoAbstract.php | 35 + .../DAVACL/Exception/NotRecognizedPrincipal.php | 35 + .../DAVACL/Exception/NotSupportedPrivilege.php | 35 + calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php | 74 + .../SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php | 77 + .../lib/OldSabre/DAVACL/IPrincipalCollection.php | 42 + .../lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php | 1402 ++++ .../lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php | 281 + .../DAVACL/PrincipalBackend/AbstractBackend.php | 18 + .../DAVACL/PrincipalBackend/BackendInterface.php | 153 + .../lib/OldSabre/DAVACL/PrincipalBackend/PDO.php | 428 ++ .../lib/OldSabre/DAVACL/PrincipalCollection.php | 33 + .../SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php | 211 + .../OldSabre/DAVACL/Property/AclRestrictions.php | 34 + .../DAVACL/Property/CurrentUserPrivilegeSet.php | 124 + .../lib/OldSabre/DAVACL/Property/Principal.php | 161 + .../DAVACL/Property/SupportedPrivilegeSet.php | 94 + .../lib/SabreDAV/lib/OldSabre/DAVACL/Version.php | 24 + .../lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php | 227 + .../SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php | 111 + .../lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php | 67 + .../lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php | 240 + .../lib/SabreDAV/lib/OldSabre/HTTP/Request.php | 284 + .../lib/SabreDAV/lib/OldSabre/HTTP/Response.php | 175 + calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php | 82 + .../lib/SabreDAV/lib/OldSabre/HTTP/Version.php | 24 + calendar/lib/SabreDAV/lib/OldSabre/autoload.php | 25 + calendar/lib/SabreDAV/vendor/autoload.php | 7 + .../lib/SabreDAV/vendor/composer/ClassLoader.php | 387 ++ .../SabreDAV/vendor/composer/autoload_classmap.php | 9 + .../vendor/composer/autoload_namespaces.php | 15 + .../lib/SabreDAV/vendor/composer/autoload_psr4.php | 9 + .../lib/SabreDAV/vendor/composer/autoload_real.php | 50 + .../lib/SabreDAV/vendor/composer/installed.json | 52 + .../lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog | 70 + .../lib/SabreDAV/vendor/oldsabre/vobject/LICENSE | 27 + .../lib/SabreDAV/vendor/oldsabre/vobject/README.md | 384 ++ .../SabreDAV/vendor/oldsabre/vobject/composer.json | 31 + .../vobject/lib/Sabre/VObject/Component.php | 405 ++ .../vobject/lib/Sabre/VObject/Component/VAlarm.php | 108 + .../lib/Sabre/VObject/Component/VCalendar.php | 244 + .../vobject/lib/Sabre/VObject/Component/VCard.php | 107 + .../vobject/lib/Sabre/VObject/Component/VEvent.php | 70 + .../lib/Sabre/VObject/Component/VFreeBusy.php | 68 + .../lib/Sabre/VObject/Component/VJournal.php | 46 + .../vobject/lib/Sabre/VObject/Component/VTodo.php | 68 + .../vobject/lib/Sabre/VObject/DateTimeParser.php | 181 + .../vobject/lib/Sabre/VObject/Document.php | 109 + .../vobject/lib/Sabre/VObject/ElementList.php | 172 + .../lib/Sabre/VObject/FreeBusyGenerator.php | 322 + .../oldsabre/vobject/lib/Sabre/VObject/Node.php | 187 + .../vobject/lib/Sabre/VObject/Parameter.php | 100 + .../vobject/lib/Sabre/VObject/ParseException.php | 12 + .../vobject/lib/Sabre/VObject/Property.php | 442 ++ .../lib/Sabre/VObject/Property/Compound.php | 125 + .../lib/Sabre/VObject/Property/DateTime.php | 245 + .../lib/Sabre/VObject/Property/MultiDateTime.php | 180 + .../oldsabre/vobject/lib/Sabre/VObject/Reader.php | 223 + .../lib/Sabre/VObject/RecurrenceIterator.php | 1112 ++++ .../lib/Sabre/VObject/Splitter/ICalendar.php | 111 + .../Sabre/VObject/Splitter/SplitterInterface.php | 39 + .../vobject/lib/Sabre/VObject/Splitter/VCard.php | 76 + .../vobject/lib/Sabre/VObject/StringUtil.php | 61 + .../vobject/lib/Sabre/VObject/TimeZoneUtil.php | 482 ++ .../oldsabre/vobject/lib/Sabre/VObject/Version.php | 24 + .../vobject/lib/Sabre/VObject/includes.php | 41 + calendar/lib/caldav-client.php | 379 ++ calendar/lib/calendar_itip.php | 240 + calendar/lib/calendar_recurrence.php | 88 + calendar/lib/calendar_ui.php | 918 +++ calendar/lib/encryption.php | 166 + calendar/lib/js/fullcalendar.js | 6845 ++++++++++++++++++++ calendar/localization/bg_BG.inc | 122 + calendar/localization/ca_ES.inc | 266 + calendar/localization/cs_CZ.inc | 273 + calendar/localization/da_DK.inc | 274 + calendar/localization/de_CH.inc | 184 + calendar/localization/de_DE.inc | 284 + calendar/localization/en_US.inc | 315 + calendar/localization/es_AR.inc | 269 + calendar/localization/es_ES.inc | 26 + calendar/localization/fi_FI.inc | 274 + calendar/localization/fr_FR.inc | 271 + calendar/localization/hu_HU.inc | 216 + calendar/localization/it_IT.inc | 273 + calendar/localization/ja_JP.inc | 177 + calendar/localization/nl_NL.inc | 229 + calendar/localization/pl_PL.inc | 272 + calendar/localization/pt_BR.inc | 213 + calendar/localization/pt_PT.inc | 274 + calendar/localization/ru_RU.inc | 277 + calendar/localization/sk_SK.inc | 82 + calendar/localization/sl_SI.inc | 273 + calendar/localization/sv_SE.inc | 273 + calendar/localization/th_TH.inc | 257 + calendar/localization/uk_UA.inc | 228 + calendar/localization/zh_CN.inc | 76 + calendar/print.js | 176 + calendar/skins/larry/README | 11 + calendar/skins/larry/calendar.css | 2327 +++++++ calendar/skins/larry/fullcalendar.css | 711 ++ calendar/skins/larry/iehacks.css | 77 + calendar/skins/larry/images/attendee-status.png | Bin 0 -> 2202 bytes calendar/skins/larry/images/autocomplete.png | Bin 0 -> 558 bytes calendar/skins/larry/images/badge_cancelled.png | Bin 0 -> 924 bytes calendar/skins/larry/images/badge_confidential.png | Bin 0 -> 1522 bytes calendar/skins/larry/images/badge_private.png | Bin 0 -> 1346 bytes calendar/skins/larry/images/calendar.png | Bin 0 -> 613 bytes calendar/skins/larry/images/calendars.png | Bin 0 -> 2582 bytes calendar/skins/larry/images/eventicons.png | Bin 0 -> 217 bytes calendar/skins/larry/images/focusview.png | Bin 0 -> 4224 bytes calendar/skins/larry/images/freebusy-colors.png | Bin 0 -> 302 bytes calendar/skins/larry/images/ical-attachment.png | Bin 0 -> 492 bytes calendar/skins/larry/images/invitation.png | Bin 0 -> 1485 bytes calendar/skins/larry/images/loading_blue.gif | Bin 0 -> 847 bytes calendar/skins/larry/images/sendinvitation.png | Bin 0 -> 337 bytes calendar/skins/larry/images/toggle.gif | Bin 0 -> 110 bytes calendar/skins/larry/images/toolbar.png | Bin 0 -> 3662 bytes calendar/skins/larry/print.css | 229 + calendar/skins/larry/print.iehacks.css | 25 + calendar/skins/larry/templates/attachment.html | 64 + calendar/skins/larry/templates/calendar.html | 537 ++ calendar/skins/larry/templates/eventedit.html | 133 + calendar/skins/larry/templates/freebusylegend.html | 7 + calendar/skins/larry/templates/itipattend.html | 37 + calendar/skins/larry/templates/kolabacl.html | 26 + calendar/skins/larry/templates/kolabform.html | 9 + calendar/skins/larry/templates/print.html | 29 + libcalendaring/README | 15 + libcalendaring/composer.json | 25 + libcalendaring/lib/.htaccess | 2 + libcalendaring/lib/Horde_Date.php | 1304 ++++ libcalendaring/lib/Horde_Date_Recurrence.php | 1705 +++++ libcalendaring/lib/OldSabre/VObject/Component.php | 405 ++ .../lib/OldSabre/VObject/Component/VAlarm.php | 108 + .../lib/OldSabre/VObject/Component/VCalendar.php | 244 + .../lib/OldSabre/VObject/Component/VCard.php | 107 + .../lib/OldSabre/VObject/Component/VEvent.php | 70 + .../lib/OldSabre/VObject/Component/VFreeBusy.php | 68 + .../lib/OldSabre/VObject/Component/VJournal.php | 46 + .../lib/OldSabre/VObject/Component/VTodo.php | 68 + .../lib/OldSabre/VObject/DateTimeParser.php | 181 + libcalendaring/lib/OldSabre/VObject/Document.php | 109 + .../lib/OldSabre/VObject/ElementList.php | 172 + .../lib/OldSabre/VObject/FreeBusyGenerator.php | 322 + libcalendaring/lib/OldSabre/VObject/Node.php | 187 + libcalendaring/lib/OldSabre/VObject/Parameter.php | 91 + .../lib/OldSabre/VObject/ParseException.php | 12 + libcalendaring/lib/OldSabre/VObject/Property.php | 453 ++ .../lib/OldSabre/VObject/Property/Compound.php | 125 + .../lib/OldSabre/VObject/Property/DateTime.php | 245 + .../OldSabre/VObject/Property/MultiDateTime.php | 180 + libcalendaring/lib/OldSabre/VObject/Reader.php | 223 + .../lib/OldSabre/VObject/RecurrenceIterator.php | 1144 ++++ .../lib/OldSabre/VObject/Splitter/ICalendar.php | 111 + .../VObject/Splitter/SplitterInterface.php | 39 + .../lib/OldSabre/VObject/Splitter/VCard.php | 76 + libcalendaring/lib/OldSabre/VObject/StringUtil.php | 61 + .../lib/OldSabre/VObject/TimeZoneUtil.php | 527 ++ libcalendaring/lib/OldSabre/VObject/Version.php | 24 + libcalendaring/lib/OldSabre/VObject/includes.php | 41 + libcalendaring/lib/libcalendaring_itip.php | 817 +++ libcalendaring/lib/libcalendaring_recurrence.php | 155 + libcalendaring/libcalendaring.js | 1192 ++++ libcalendaring/libcalendaring.php | 1637 +++++ libcalendaring/libvcalendar.php | 1362 ++++ libcalendaring/localization/bg_BG.inc | 25 + libcalendaring/localization/ca_ES.inc | 15 + libcalendaring/localization/cs_CZ.inc | 131 + libcalendaring/localization/da_DK.inc | 85 + libcalendaring/localization/de_CH.inc | 81 + libcalendaring/localization/de_DE.inc | 135 + libcalendaring/localization/en_US.inc | 165 + libcalendaring/localization/es_AR.inc | 125 + libcalendaring/localization/es_ES.inc | 59 + libcalendaring/localization/et_EE.inc | 60 + libcalendaring/localization/fi_FI.inc | 133 + libcalendaring/localization/fr_FR.inc | 131 + libcalendaring/localization/hu_HU.inc | 96 + libcalendaring/localization/it_IT.inc | 85 + libcalendaring/localization/ja_JP.inc | 77 + libcalendaring/localization/nl_NL.inc | 89 + libcalendaring/localization/pl_PL.inc | 12 + libcalendaring/localization/pt_BR.inc | 85 + libcalendaring/localization/pt_PT.inc | 12 + libcalendaring/localization/ru_RU.inc | 139 + libcalendaring/localization/sk_SK.inc | 9 + libcalendaring/localization/sl_SI.inc | 15 + libcalendaring/localization/sv_SE.inc | 12 + libcalendaring/localization/th_TH.inc | 80 + libcalendaring/localization/uk_UA.inc | 14 + libcalendaring/localization/zh_CN.inc | 10 + libcalendaring/skins/larry/libcal.css | 166 + 665 files changed, 125761 insertions(+) create mode 100644 README.md create mode 100644 calendar/.gitignore create mode 100644 calendar/README_kolab create mode 100644 calendar/TODO create mode 100644 calendar/UPGRADING create mode 100644 calendar/calendar.php create mode 100644 calendar/calendar_base.js create mode 100644 calendar/calendar_ui.js create mode 100644 calendar/composer.json create mode 100644 calendar/config.inc.php.dist create mode 100644 calendar/drivers/caldav/SQL/mysql.initial.sql create mode 100644 calendar/drivers/caldav/SQL/mysql/.keep_dir create mode 100644 calendar/drivers/caldav/SQL/mysql/2014081300.sql create mode 100644 calendar/drivers/caldav/SQL/mysql/2015022500.sql create mode 100644 calendar/drivers/caldav/SQL/mysql/2015022700.sql create mode 100644 calendar/drivers/caldav/SQL/postgres.initial.sql create mode 100644 calendar/drivers/caldav/caldav_driver.php create mode 100644 calendar/drivers/caldav/caldav_sync.php create mode 100644 calendar/drivers/calendar_driver.php create mode 100644 calendar/drivers/database/SQL/mysql.initial.sql create mode 100644 calendar/drivers/database/SQL/mysql/2012080600.sql create mode 100644 calendar/drivers/database/SQL/mysql/2013011000.sql create mode 100644 calendar/drivers/database/SQL/mysql/2013042700.sql create mode 100644 calendar/drivers/database/SQL/mysql/2013051600.sql create mode 100644 calendar/drivers/database/SQL/mysql/2014040900.sql create mode 100644 calendar/drivers/database/SQL/mysql/2015022700.sql create mode 100644 calendar/drivers/database/SQL/postgres.initial.sql create mode 100644 calendar/drivers/database/SQL/postgres/2012080600.sql create mode 100644 calendar/drivers/database/SQL/postgres/2013011000.sql create mode 100644 calendar/drivers/database/SQL/postgres/2013042700.sql create mode 100644 calendar/drivers/database/SQL/postgres/2013051600.sql create mode 100644 calendar/drivers/database/SQL/postgres/2014040900.sql create mode 100644 calendar/drivers/database/SQL/postgres/2015022700.sql create mode 100644 calendar/drivers/database/SQL/sqlite.initial.sql create mode 100644 calendar/drivers/database/SQL/sqlite/2013011000.sql create mode 100644 calendar/drivers/database/SQL/sqlite/2013042700.sql create mode 100644 calendar/drivers/database/SQL/sqlite/2013051600.sql create mode 100644 calendar/drivers/database/SQL/sqlite/2014040900.sql create mode 100644 calendar/drivers/database/SQL/sqlite/2015022700.sql create mode 100644 calendar/drivers/database/database_driver.php create mode 100644 calendar/drivers/ical/SQL/mysql.initial.sql create mode 100644 calendar/drivers/ical/SQL/mysql/.keep_dir create mode 100644 calendar/drivers/ical/SQL/mysql/2015022500.sql create mode 100644 calendar/drivers/ical/SQL/mysql/2015022700.sql create mode 100644 calendar/drivers/ical/ical_driver.php create mode 100644 calendar/drivers/ical/ical_sync.php create mode 100644 calendar/drivers/kolab/SQL/mysql.initial.sql create mode 100644 calendar/drivers/kolab/SQL/mysql/2012080600.sql create mode 100644 calendar/drivers/kolab/SQL/mysql/2013011000.sql create mode 100644 calendar/drivers/kolab/SQL/mysql/2014041700.sql create mode 100644 calendar/drivers/kolab/SQL/mysql/2014082600.sql create mode 100644 calendar/drivers/kolab/SQL/oracle.initial.sql create mode 100644 calendar/drivers/kolab/SQL/postgres.initial.sql create mode 100644 calendar/drivers/kolab/kolab_calendar.php create mode 100644 calendar/drivers/kolab/kolab_driver.php create mode 100644 calendar/drivers/kolab/kolab_invitation_calendar.php create mode 100644 calendar/drivers/kolab/kolab_user_calendar.php create mode 100644 calendar/drivers/ldap/resources_driver_ldap.php create mode 100644 calendar/drivers/resources_driver.php create mode 120000 calendar/helpdocs/en_US/_static/_skin create mode 120000 calendar/helpdocs/en_US/_static/kolab/alarms-popup.png create mode 100644 calendar/helpdocs/en_US/_static/kolab/calendar-acl.png create mode 100644 calendar/helpdocs/en_US/_static/kolab/calendar-header.png create mode 100644 calendar/helpdocs/en_US/_static/kolab/event-participants.png create mode 120000 calendar/helpdocs/en_US/_static/kolab/event-resize.png create mode 120000 calendar/helpdocs/en_US/_static/kolab/itip-invitation.png create mode 120000 calendar/helpdocs/en_US/_static/kolab/itip-reply.png create mode 100644 calendar/helpdocs/en_US/_static/larry/alarms-popup.png create mode 100644 calendar/helpdocs/en_US/_static/larry/calendar-acl.png create mode 100644 calendar/helpdocs/en_US/_static/larry/calendar-header.png create mode 100644 calendar/helpdocs/en_US/_static/larry/event-participants.png create mode 100644 calendar/helpdocs/en_US/_static/larry/event-resize.png create mode 100644 calendar/helpdocs/en_US/_static/larry/itip-invitation.png create mode 100644 calendar/helpdocs/en_US/_static/larry/itip-reply.png create mode 100644 calendar/helpdocs/en_US/importexport.rst create mode 100644 calendar/helpdocs/en_US/index.rst create mode 100644 calendar/helpdocs/en_US/invitations.rst create mode 100644 calendar/helpdocs/en_US/manage.rst create mode 100644 calendar/helpdocs/en_US/overview.rst create mode 100644 calendar/helpdocs/en_US/settings.rst create mode 100644 calendar/helpdocs/en_US/sharing.rst create mode 100644 calendar/helpdocs/locale/bg_BG/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/bg_BG/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/bg_BG/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/bg_BG/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/bg_BG/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/bg_BG/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/bg_BG/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/ca_ES/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/ca_ES/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/ca_ES/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/ca_ES/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/ca_ES/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/ca_ES/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/ca_ES/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/da_DK/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/da_DK/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/da_DK/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/da_DK/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/da_DK/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/da_DK/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/da_DK/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/de_CH/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/de_CH/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/de_CH/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/de_CH/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/de_CH/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/de_CH/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/de_CH/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/de_DE/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/de_DE/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/de_DE/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/de_DE/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/de_DE/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/de_DE/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/de_DE/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/en_US/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/en_US/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/en_US/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/en_US/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/en_US/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/en_US/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/en_US/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/es_AR/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/es_AR/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/es_AR/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/es_AR/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/es_AR/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/es_AR/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/es_AR/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/es_ES/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/es_ES/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/es_ES/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/es_ES/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/es_ES/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/es_ES/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/es_ES/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/et_EE/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/et_EE/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/et_EE/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/et_EE/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/et_EE/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/et_EE/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/et_EE/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/fi_FI/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/fi_FI/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/fi_FI/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/fi_FI/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/fi_FI/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/fi_FI/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/fi_FI/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/fr_FR/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/fr_FR/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/fr_FR/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/fr_FR/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/fr_FR/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/fr_FR/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/fr_FR/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/he/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/he/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/he/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/he/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/he/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/he/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/he/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/hr/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/hr/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/hr/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/hr/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/hr/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/hr/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/hr/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/hu_HU/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/hu_HU/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/hu_HU/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/hu_HU/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/hu_HU/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/hu_HU/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/hu_HU/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/it_IT/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/it_IT/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/it_IT/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/it_IT/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/it_IT/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/it_IT/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/it_IT/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/ja_JP/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/ja_JP/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/ja_JP/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/ja_JP/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/ja_JP/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/ja_JP/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/ja_JP/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/nl_NL/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/nl_NL/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/nl_NL/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/nl_NL/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/nl_NL/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/nl_NL/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/nl_NL/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/pl_PL/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/pl_PL/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/pl_PL/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/pl_PL/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/pl_PL/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/pl_PL/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/pl_PL/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/pt_BR/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/pt_BR/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/pt_BR/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/pt_BR/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/pt_BR/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/pt_BR/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/pt_BR/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/ro/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/ro/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/ro/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/ro/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/ro/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/ro/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/ro/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/ru_RU/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/ru_RU/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/ru_RU/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/ru_RU/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/ru_RU/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/ru_RU/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/ru_RU/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/sk/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/sk/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/sk/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/sk/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/sk/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/sk/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/sk/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/sv/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/sv/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/sv/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/sv/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/sv/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/sv/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/sv/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/sv_SE/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/sv_SE/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/sv_SE/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/sv_SE/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/sv_SE/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/sv_SE/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/sv_SE/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/tr_TR/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/tr_TR/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/tr_TR/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/tr_TR/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/tr_TR/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/tr_TR/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/tr_TR/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/uk/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/uk/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/uk/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/uk/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/uk/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/uk/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/uk/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/vi/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/vi/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/vi/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/vi/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/vi/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/vi/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/vi/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/vi_VN/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/vi_VN/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/vi_VN/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/vi_VN/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/vi_VN/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/vi_VN/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/vi_VN/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/zh_CN/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/zh_CN/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/zh_CN/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/zh_CN/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/zh_CN/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/zh_CN/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/zh_CN/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/locale/zh_TW/LC_MESSAGES/importexport.po create mode 100644 calendar/helpdocs/locale/zh_TW/LC_MESSAGES/index.po create mode 100644 calendar/helpdocs/locale/zh_TW/LC_MESSAGES/invitations.po create mode 100644 calendar/helpdocs/locale/zh_TW/LC_MESSAGES/manage.po create mode 100644 calendar/helpdocs/locale/zh_TW/LC_MESSAGES/overview.po create mode 100644 calendar/helpdocs/locale/zh_TW/LC_MESSAGES/settings.po create mode 100644 calendar/helpdocs/locale/zh_TW/LC_MESSAGES/sharing.po create mode 100644 calendar/helpdocs/po/importexport.pot create mode 100644 calendar/helpdocs/po/index.pot create mode 100644 calendar/helpdocs/po/invitations.pot create mode 100644 calendar/helpdocs/po/manage.pot create mode 100644 calendar/helpdocs/po/overview.pot create mode 100644 calendar/helpdocs/po/settings.pot create mode 100644 calendar/helpdocs/po/sharing.pot create mode 100644 calendar/lib/SabreDAV/.htaccess create mode 100644 calendar/lib/SabreDAV/ChangeLog create mode 100644 calendar/lib/SabreDAV/LICENSE create mode 100644 calendar/lib/SabreDAV/README.md create mode 100644 calendar/lib/SabreDAV/composer.json create mode 100644 calendar/lib/SabreDAV/composer.lock create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.ico create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.png create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.png create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.png create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.png create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.png create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.png create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.png create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php create mode 100644 calendar/lib/SabreDAV/lib/OldSabre/autoload.php create mode 100644 calendar/lib/SabreDAV/vendor/autoload.php create mode 100644 calendar/lib/SabreDAV/vendor/composer/ClassLoader.php create mode 100644 calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php create mode 100644 calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php create mode 100644 calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php create mode 100644 calendar/lib/SabreDAV/vendor/composer/autoload_real.php create mode 100644 calendar/lib/SabreDAV/vendor/composer/installed.json create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php create mode 100644 calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php create mode 100644 calendar/lib/caldav-client.php create mode 100644 calendar/lib/calendar_itip.php create mode 100644 calendar/lib/calendar_recurrence.php create mode 100644 calendar/lib/calendar_ui.php create mode 100644 calendar/lib/encryption.php create mode 100644 calendar/lib/js/fullcalendar.js create mode 100644 calendar/localization/bg_BG.inc create mode 100644 calendar/localization/ca_ES.inc create mode 100644 calendar/localization/cs_CZ.inc create mode 100644 calendar/localization/da_DK.inc create mode 100644 calendar/localization/de_CH.inc create mode 100644 calendar/localization/de_DE.inc create mode 100644 calendar/localization/en_US.inc create mode 100644 calendar/localization/es_AR.inc create mode 100644 calendar/localization/es_ES.inc create mode 100644 calendar/localization/fi_FI.inc create mode 100644 calendar/localization/fr_FR.inc create mode 100644 calendar/localization/hu_HU.inc create mode 100644 calendar/localization/it_IT.inc create mode 100644 calendar/localization/ja_JP.inc create mode 100644 calendar/localization/nl_NL.inc create mode 100644 calendar/localization/pl_PL.inc create mode 100644 calendar/localization/pt_BR.inc create mode 100644 calendar/localization/pt_PT.inc create mode 100644 calendar/localization/ru_RU.inc create mode 100644 calendar/localization/sk_SK.inc create mode 100644 calendar/localization/sl_SI.inc create mode 100644 calendar/localization/sv_SE.inc create mode 100644 calendar/localization/th_TH.inc create mode 100644 calendar/localization/uk_UA.inc create mode 100644 calendar/localization/zh_CN.inc create mode 100644 calendar/print.js create mode 100644 calendar/skins/larry/README create mode 100644 calendar/skins/larry/calendar.css create mode 100644 calendar/skins/larry/fullcalendar.css create mode 100644 calendar/skins/larry/iehacks.css create mode 100644 calendar/skins/larry/images/attendee-status.png create mode 100644 calendar/skins/larry/images/autocomplete.png create mode 100644 calendar/skins/larry/images/badge_cancelled.png create mode 100644 calendar/skins/larry/images/badge_confidential.png create mode 100644 calendar/skins/larry/images/badge_private.png create mode 100644 calendar/skins/larry/images/calendar.png create mode 100644 calendar/skins/larry/images/calendars.png create mode 100644 calendar/skins/larry/images/eventicons.png create mode 100644 calendar/skins/larry/images/focusview.png create mode 100644 calendar/skins/larry/images/freebusy-colors.png create mode 100644 calendar/skins/larry/images/ical-attachment.png create mode 100644 calendar/skins/larry/images/invitation.png create mode 100644 calendar/skins/larry/images/loading_blue.gif create mode 100644 calendar/skins/larry/images/sendinvitation.png create mode 100644 calendar/skins/larry/images/toggle.gif create mode 100644 calendar/skins/larry/images/toolbar.png create mode 100644 calendar/skins/larry/print.css create mode 100644 calendar/skins/larry/print.iehacks.css create mode 100644 calendar/skins/larry/templates/attachment.html create mode 100644 calendar/skins/larry/templates/calendar.html create mode 100644 calendar/skins/larry/templates/eventedit.html create mode 100644 calendar/skins/larry/templates/freebusylegend.html create mode 100644 calendar/skins/larry/templates/itipattend.html create mode 100644 calendar/skins/larry/templates/kolabacl.html create mode 100644 calendar/skins/larry/templates/kolabform.html create mode 100644 calendar/skins/larry/templates/print.html create mode 100644 libcalendaring/README create mode 100644 libcalendaring/composer.json create mode 100644 libcalendaring/lib/.htaccess create mode 100644 libcalendaring/lib/Horde_Date.php create mode 100644 libcalendaring/lib/Horde_Date_Recurrence.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VCard.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VEvent.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VJournal.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Component/VTodo.php create mode 100644 libcalendaring/lib/OldSabre/VObject/DateTimeParser.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Document.php create mode 100644 libcalendaring/lib/OldSabre/VObject/ElementList.php create mode 100644 libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Node.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Parameter.php create mode 100644 libcalendaring/lib/OldSabre/VObject/ParseException.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property/Compound.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property/DateTime.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Reader.php create mode 100644 libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Splitter/VCard.php create mode 100644 libcalendaring/lib/OldSabre/VObject/StringUtil.php create mode 100644 libcalendaring/lib/OldSabre/VObject/TimeZoneUtil.php create mode 100644 libcalendaring/lib/OldSabre/VObject/Version.php create mode 100644 libcalendaring/lib/OldSabre/VObject/includes.php create mode 100644 libcalendaring/lib/libcalendaring_itip.php create mode 100644 libcalendaring/lib/libcalendaring_recurrence.php create mode 100644 libcalendaring/libcalendaring.js create mode 100644 libcalendaring/libcalendaring.php create mode 100644 libcalendaring/libvcalendar.php create mode 100644 libcalendaring/localization/bg_BG.inc create mode 100644 libcalendaring/localization/ca_ES.inc create mode 100644 libcalendaring/localization/cs_CZ.inc create mode 100644 libcalendaring/localization/da_DK.inc create mode 100644 libcalendaring/localization/de_CH.inc create mode 100644 libcalendaring/localization/de_DE.inc create mode 100644 libcalendaring/localization/en_US.inc create mode 100644 libcalendaring/localization/es_AR.inc create mode 100644 libcalendaring/localization/es_ES.inc create mode 100644 libcalendaring/localization/et_EE.inc create mode 100644 libcalendaring/localization/fi_FI.inc create mode 100644 libcalendaring/localization/fr_FR.inc create mode 100644 libcalendaring/localization/hu_HU.inc create mode 100644 libcalendaring/localization/it_IT.inc create mode 100644 libcalendaring/localization/ja_JP.inc create mode 100644 libcalendaring/localization/nl_NL.inc create mode 100644 libcalendaring/localization/pl_PL.inc create mode 100644 libcalendaring/localization/pt_BR.inc create mode 100644 libcalendaring/localization/pt_PT.inc create mode 100644 libcalendaring/localization/ru_RU.inc create mode 100644 libcalendaring/localization/sk_SK.inc create mode 100644 libcalendaring/localization/sl_SI.inc create mode 100644 libcalendaring/localization/sv_SE.inc create mode 100644 libcalendaring/localization/th_TH.inc create mode 100644 libcalendaring/localization/uk_UA.inc create mode 100644 libcalendaring/localization/zh_CN.inc create mode 100644 libcalendaring/skins/larry/libcal.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca01bd2 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# Roundcube Calendar plugin with CalDAV support + +Using Kolab-plugins from https://git.kolab.org/ (based on v3.2.9) +Using the CalDAV driver from https://gitlab.awesome-it.de/kolab/roundcube-plugins/tree/feature_caldav (based on v3.2.8) + +Hosted at https://git.faster-it.com/roundcube_calendar +Mirrored at https://github.com/fasterit/roundcube_calendar + + +## About + +Roundcube is an awesome web mailer and a good platform for calendaring as the work from +Kolab Systems AG shows. Their Roundcube plugins (primarily developed to support the +Kolab Groupware server) have been extended by "Awesome IT" (a GbR out for Karlsruhe, Germany) +to support CalDAV calendars. + +Getting this all to work is a bit of a hassle so we - Faster IT GmbH - decided to +combine the various parts into a distribution when we were asked to implement this for a nonprofit company. +For the public benefit we re-publish the work under the origiginal AGPLv3+ license. + +This version is compatible with the [rcmcarddav plugin](https://github.com/blind-coder/rcmcarddav) +from [Benjamin Schieder](http://www.benjamin-schieder.de/carddav.html). +We hacked this by moving the ancient SabreDAV version the WebDAV driver uses into its own +`OldSabre` namespace so it does not collide with rcmcarddav's version. +We updated it to 1.8.12 which is the last version of the 1.x series (SabreDAV is at 3.1 now). + +That way you can have both CalDAV and CardDAV supported simultaneously in Roundcube by Free Software. + +There is work underway at "Awesome IT" to update SabreDAV to a recent version +(see their [gitlab](https://gitlab.awesome-it.de/kolab/roundcube-plugins/commit/5a0825b89a0b0183bf8469e66b667e294309b609)) +but at the time of writing this (2016-03-07) it has not yet been completed. + + +## Installation + +Clone the repository and copy `calendar` and `libcalendaring` to the `plugins` directory +of your roundcube installation: + + $ git clone git://github.com/fasterit/roundcube_calendar.git + $ cd roundcube_calendar + $ cd -r calendar/ libcalendaring/ /var/www/htdocs/roundcube/plugins/ # adjust as needed + +Copy and edit the supplied config.inc.php.dist: + + $ cd /var/www/htdocs/roundcube/plugins/ + $ cp config.inc.php.dist config.inc.php + $ $EDITOR config.inc.php + +Change the driver to include caldav (this is why you use this version of the calendar +plugins, right?); + + // backend type (database, kolab, caldav, ical) + $config['calendar_driver'] = array('database', 'caldav'); + $config['calendar_driver_default'] = 'caldav'; + +Provide a unique crypt_key. This is used to scramble the passwords for CalDAV calendars +before they are stored into the database: + + // Crypt key to encrypt passwords for added iCAL/CalDAV calendars + $config['calendar_crypt_key'] = ""; + +Now we need to import the database tables for the database driver (these tables are also required +for CalDAV!) and the caldav driver. Your database name () will usually be roundcubemail or roundcube. +Check with `mysql -e 'show databases;'`. + + $ mysql < /var/www/htdocs/roundcube/plugins/calendar/drivers/database/SQL/mysql.initial.sql + $ mysql < /var/www/htdocs/roundcube/plugins/calendar/drivers/caldav/SQL/mysql.initial.sql + +Finally enable the calendar plugin in your **global** roundcube `config.inc.php`: + + $ $EDITOR /var/www/htdocs/roundcube/config/config.inc.php + +Add 'calendar' to the list of active plugins: + + $config['plugins'] = array( + (...) + 'calendar', + ); + + +## Usage + +After installation, add a CalDAV Calendar by going to the Calendar view and clicking the [+] in the +lower right corner. Select CalDAV Calendar and provide a name, URL and username / password for your calendar. + +In case you want to add calendars that are served with self-signed SSL certificates, be sure to set + + // Set to false to allow CURL to connect with SSL hosts that it can't verify the certificates from + // e.g. for self-signed certificates. + // technical note: This sets CURLOPT_SSL_VERIFYPEER _and_ CURLOPT_SSL_VERIFYHOST. + $config['calendar_curl_secure_ssl'] = false; + +in `calendar/config.inc.php`. And know what you are doing. + + +## Debugging + +See `/var/www/htdocs/roundcube/logs/` for errors. + +## Support + +There is none. Really. This code is not for the faint of heart. +Patches welcome. diff --git a/calendar/.gitignore b/calendar/.gitignore new file mode 100644 index 0000000..7c2f14c --- /dev/null +++ b/calendar/.gitignore @@ -0,0 +1,8 @@ +*.swp +*.bak +*.old +*~ +config.inc.php +skins/* +!skins/default +!skins/larry \ No newline at end of file diff --git a/calendar/README_kolab b/calendar/README_kolab new file mode 100644 index 0000000..2426fcf --- /dev/null +++ b/calendar/README_kolab @@ -0,0 +1,78 @@ +A calendar module for Roundcube +------------------------------- + +This plugin currently supports a local database as well as a Kolab groupware +server as backends for calendar and event storage. For both drivers, some +initialization of the local database is necessary. To do so, execute the +SQL commands in drivers//SQL/.initial.sql + +The client-side calendar UI relies on the "fullcalendar" project by Adam Arshaw +with extensions made for the use in Roundcube. All changes are published in +an official fork at https://github.com/roundcube/fullcalendar + +For some general calendar-based operations such as alarms handling or iCal +parsing/exporting this plugins requires the `libcalendaring` plugin which +is also part of the Kolab Roundcube Plugins repository. Make sure that plugin +is installed and configured correctly. + +For recurring event computation, some utility classes from the Horde project +are used. They are packaged in a slightly modified version with this plugin. + +IMPORTANT +--------- + +The calendar module makes heavy use of PHP's DateTime as well as DateInterval +classes. The latter one requires at least PHP 5.3.0 to run. + + +REQUIREMENTS +------------ + +Some functions are shared with other plugins and therefore being moved to +library plugins. Thus in order to run the calendar plugin, you also need the +following plugins installed: + +* libcalendaring [1] +* libkolab [1] (when using the 'kolab' driver) + + +INSTALLATION +------------ + +For a manual installation of the calendar plugin (and its dependencies), +execute the following steps. This will set it up with the database backend +driver. + +1. Get the source from git + + $ cd /tmp + $ git clone git://git.kolab.org/git/roundcubemail-plugins-kolab + $ cd //plugins + $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/calendar . + $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/libcalendaring . + +2. Create calendar plugin configuration + + $ cd calendar/ + $ cp config.inc.php.dist config.inc.php + $ edit config.inc.php + +3. Initialize the calendar database tables + + $ mysql roundcubemail < drivers/database/SQL/mysql.initial.sql + +4. Enable the calendar plugin + + $ cd ../../ + $ edit config/config.inc.php + +Add 'calendar' to the list of active plugins: + + $config['plugins'] = array( + (...) + 'calendar', + ); + + + +[1] http://git.kolab.org/roundcubemail-plugins-kolab/ diff --git a/calendar/TODO b/calendar/TODO new file mode 100644 index 0000000..b1a08d7 --- /dev/null +++ b/calendar/TODO @@ -0,0 +1,48 @@ ++ Edit: 3.12: Subject ++ Edit: 3.13: Location ++ Edit: 3.14: Start / End / All Day ++ Edit: 3.15: Show time as: Busy, Free, Out of office ++ Edit: 3.16: Reminder set ++ Edit: 3.17: Priority: High/Low ++ Edit: 3.18: Recurrence (in line with Kontact) ++ Edit: 3.19: Attachment Upload ++ Edit: 3.20: Print ++ Add/Manage Attendees + + Edit: 3.21: Required / Optional / Resource specification + + Edit: 3.22: Conflict Handling (Free/Busy Check for attendees) ++ View: 3.3: Display modes (agenda / day / week / month) + + Day / Week / Month + + List (Agenda) view + + Add selection for date range + - Individual days selection ++ Show list of calendars in a (hideable) drawer + + View: 3.1: Folder list + + View: 3.2: Add / Remove / Rename / Share Folders + + View: 3.6: Combined calendar view (Turn calendars on/off) + + View: 3.7: Small month overview calendar ++ View: 3.5: Search + - Filter by categories (similar to mail) ++ View: 3.9: Alter event with drag/drop ++ Option: 4.12: Set default reminder time ++ Option: 3.23: Specify folder for new event (prefs) ++ Option: Set date/time format in prefs ++ Receive: 1.20: Invitation handling + - Jump to calendar view from mail ("Show event") + - Allow to re-send invitations + - Implement iTIP delegation + ++ View: 3.4: Fish-Eye View For Busy Days ++ View: 3.8: Color according to calendar and category (similar to Kontact) + ++ Support for multiple calendars (replace categories) ++ Allow user to create/edit/delete calendars ++ Colors for calendars should be user-configurable ++ ICS parser/generator (http://code.google.com/p/qcal/) + +- Script to send event alarms by email (in cronjob) +- Export *with* attachments +- Remember last visited view +- Create/manage invdividual views ++ Importing ICS files (upload, drag & drop) + + diff --git a/calendar/UPGRADING b/calendar/UPGRADING new file mode 100644 index 0000000..0e36e85 --- /dev/null +++ b/calendar/UPGRADING @@ -0,0 +1,17 @@ +UPGRADING instructions +====================== + +To update database schema, depending on the driver you're using, +please run in Roundcube bin/ directory: + +updatedb.sh --package=calendar- --version= \ + --dir=../plugins/calendar/drivers//SQL + +[*] Replace with "database" or "kolab" (without quotes) +[*] Replace with Roundcube version e.g. 0.9.0 +[*] Roundcube should be upgraded before plugin upgrades + +Example: + +updatedb.sh --package=calendar-kolab --version=0.9.0 \ + --dir=../plugins/calendar/drivers/kolab/SQL diff --git a/calendar/calendar.php b/calendar/calendar.php new file mode 100644 index 0000000..cb32773 --- /dev/null +++ b/calendar/calendar.php @@ -0,0 +1,3581 @@ + + * @author Thomas Bruederli + * + * Copyright (C) 2010, Lazlo Westerhof + * Copyright (C) 2014-2015, Kolab Systems AG + * + * 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 . + */ + +class calendar extends rcube_plugin +{ + const FREEBUSY_UNKNOWN = 0; + const FREEBUSY_FREE = 1; + const FREEBUSY_BUSY = 2; + const FREEBUSY_TENTATIVE = 3; + const FREEBUSY_OOF = 4; + + const SESSION_KEY = 'calendar_temp'; + + public $task = '?(?!logout).*'; + public $rc; + public $lib; + private $_drivers = null; + private $_cals = null; + private $_cal_driver_map = null; + public $resources_dir; + public $home; // declare public to be used in other classes + public $urlbase; + public $timezone; + public $timezone_offset; + public $gmt_offset; + public $ui; + + public $defaults = array( + 'calendar_default_view' => "agendaWeek", + 'calendar_timeslots' => 2, + 'calendar_work_start' => 6, + 'calendar_work_end' => 18, + 'calendar_agenda_range' => 60, + 'calendar_agenda_sections' => 'smart', + 'calendar_event_coloring' => 0, + 'calendar_time_indicator' => true, + 'calendar_allow_invite_shared' => false, + 'calendar_itip_send_option' => 3, + 'calendar_itip_after_action' => 0, + ); + + private $ical; + private $itip; + + /** + * Plugin initialization. + */ + function init() + { + $this->require_plugin('libcalendaring'); + + $this->rc = rcube::get_instance(); + $this->lib = libcalendaring::get_instance(); + + $this->register_task('calendar', 'calendar'); + + // load calendar configuration + $this->load_config(); + + // load localizations + $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print')); + + $this->timezone = $this->lib->timezone; + $this->gmt_offset = $this->lib->gmt_offset; + $this->dst_active = $this->lib->dst_active; + $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active; + + require($this->home . '/lib/calendar_ui.php'); + $this->ui = new calendar_ui($this); + + // catch iTIP confirmation requests that don're require a valid session + if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) { + $this->add_hook('startup', array($this, 'itip_attend_response')); + } + else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) { + $this->add_hook('startup', array($this, 'ical_feed_export')); + } + else { + // default startup routine + $this->add_hook('startup', array($this, 'startup')); + } + + $this->add_hook('user_delete', array($this, 'user_delete')); + } + + /** + * Startup hook + */ + public function startup($args) + { + // the calendar module can be enabled/disabled by the kolab_auth plugin + if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true)) + return; + + // load Calendar user interface + if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || $args['action'] == 'preview')) { + $this->ui->init(); + + // settings are required in (almost) every GUI step + if ($args['action'] != 'attend') + $this->rc->output->set_env('calendar_settings', $this->load_settings()); + } + + if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') { + // Load drivers to register possible hooks. + $this->load_drivers(); + + // register calendar actions + $this->register_action('index', array($this, 'calendar_view')); + $this->register_action('event', array($this, 'event_action')); + $this->register_action('calendar', array($this, 'calendar_action')); + $this->register_action('count', array($this, 'count_events')); + $this->register_action('load_events', array($this, 'load_events')); + $this->register_action('export_events', array($this, 'export_events')); + $this->register_action('import_events', array($this, 'import_events')); + $this->register_action('upload', array($this, 'attachment_upload')); + $this->register_action('get-attachment', array($this, 'attachment_get')); + $this->register_action('freebusy-status', array($this, 'freebusy_status')); + $this->register_action('freebusy-times', array($this, 'freebusy_times')); + $this->register_action('randomdata', array($this, 'generate_randomdata')); + $this->register_action('print', array($this,'print_view')); + $this->register_action('mailimportitip', array($this, 'mail_import_itip')); + $this->register_action('mailimportattach', array($this, 'mail_import_attachment')); + $this->register_action('mailtoevent', array($this, 'mail_message2event')); + $this->register_action('inlineui', array($this, 'get_inline_ui')); + $this->register_action('check-recent', array($this, 'check_recent')); + $this->register_action('itip-status', array($this, 'event_itip_status')); + $this->register_action('itip-remove', array($this, 'event_itip_remove')); + $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply')); + $this->register_action('itip-delegate', array($this, 'mail_itip_delegate')); + $this->register_action('resources-list', array($this, 'resources_list')); + $this->register_action('resources-owner', array($this, 'resources_owner')); + $this->register_action('resources-calendar', array($this, 'resources_calendar')); + $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete')); + $this->add_hook('refresh', array($this, 'refresh')); + + // remove undo information... + if ($undo = $_SESSION['calendar_event_undo']) { + // ...after timeout + $undo_time = $this->rc->config->get('undo_timeout', 0); + if ($undo['ts'] < time() - $undo_time) { + $this->rc->session->remove('calendar_event_undo'); + // @TODO: do EXPUNGE on kolab objects? + } + } + + // loading preinstalled calendars + $preinstalled_calendars = $this->rc->config->get('calendar_preinstalled_calendars', FALSE); + if ($preinstalled_calendars && is_array($preinstalled_calendars)) { + + // expanding both caldav url and user with RC (imap) username + foreach ($preinstalled_calendars as $index => $cal){ + $preinstalled_calendars[$index]['caldav_url'] = str_replace('%u', $this->rc->get_user_name(), $cal['caldav_url']); + $preinstalled_calendars[$index]['caldav_user'] = str_replace('%u', $this->rc->get_user_name(), $cal['caldav_user']); + } + + foreach ($this->get_drivers() as $driver_name => $driver) { + foreach ($preinstalled_calendars as $cal) { + if ($driver_name == $cal['driver']) { + if (!$driver->insert_default_calendar($cal)) { + $error_msg = 'Unable to add default calendars' . ($driver && $driver->last_error ? ': ' . $driver->last_error :''); + $this->rc->output->show_message($error_msg, 'error'); + } + } + } + } + } + } + else if ($args['task'] == 'settings') { + // add hooks for Calendar settings + $this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list')); + $this->add_hook('preferences_list', array($this, 'preferences_list')); + $this->add_hook('preferences_save', array($this, 'preferences_save')); + } + else if ($args['task'] == 'mail') { + // hooks to catch event invitations on incoming mails + if ($args['action'] == 'show' || $args['action'] == 'preview') { + $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); + } + + // add 'Create event' item to message menu + if ($this->api->output->type == 'html') { + $this->api->add_content(html::tag('li', null, + $this->api->output->button(array( + 'command' => 'calendar-create-from-mail', + 'label' => 'calendar.createfrommail', + 'type' => 'link', + 'classact' => 'icon calendarlink active', + 'class' => 'icon calendarlink', + 'innerclass' => 'icon calendar', + ))), + 'messagemenu'); + + $this->api->output->add_label('calendar.createfrommail'); + } + + $this->add_hook('messages_list', array($this, 'mail_messages_list')); + $this->add_hook('message_compose', array($this, 'mail_message_compose')); + } + else if ($args['task'] == 'addressbook') { + if ($this->rc->config->get('calendar_contact_birthdays')) { + $this->add_hook('contact_update', array($this, 'contact_update')); + $this->add_hook('contact_create', array($this, 'contact_update')); + } + } + + // add hooks to display alarms + $this->add_hook('pending_alarms', array($this, 'pending_alarms')); + $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms')); + } + + /** + * Helper method to load all configured drivers. + */ + public function load_drivers() + { + if($this->_drivers == null) + { + $this->_drivers = array(); + + foreach($this->get_driver_names() as $driver_name) + { + $driver_name = trim($driver_name); + $driver_class = $driver_name . '_driver'; + + require_once($this->home . '/drivers/calendar_driver.php'); + require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php'); + + if($driver_name == "kolab") + $this->require_plugin('libkolab'); + + $driver = new $driver_class($this); + + if ($driver->undelete) + $driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0; + + $this->_drivers[$driver_name] = $driver; + } + } + } + + /* + * Helper method to get configured driver names. + * @return List of driver names. + */ + public function get_driver_names() + { + $driver_names = $this->rc->config->get('calendar_driver', array('kolab')); + if(!is_array($driver_names)) $driver_names = array($driver_names); + return $driver_names; + } + + /** + * Helpers function to return loaded drivers. + * @return List of driver objects. + */ + public function get_drivers() + { + $this->load_drivers(); + return $this->_drivers; + } + + /** + * Helper method to get driver by name. + * + * @param string $name Driver name to get driver object for. + * @return mixed Driver object or null if no such driver exists. + */ + public function get_driver_by_name($name) + { + $this->load_drivers(); + if(isset($this->_drivers[$name])) + { + return $this->_drivers[$name]; + } + else + { + rcube::raise_error("Unknown driver requested \"$name\".", true, true); + return null; + } + } + + /** + * Helper method to get the driver by GPC input, e.g. "_driver" or "driver" + * property specified in POST/GET or COOKIE variables. + * + * @param boolean $quiet = false Indicates where to raise an error if no driver was found in GPC + * @return mixed Driver object or null if no such driver exists. + */ + public function get_driver_by_gpc($quiet = false) + { + $this->load_drivers(); + $driver_name = null; + foreach(array("_driver", "driver") as $input_name) + { + $driver_name = get_input_value($input_name, RCUBE_INPUT_GPC); + if($driver_name != null) break; + } + + // Remove possible postfix "_driver" from requested driver name. + $driver_name = str_replace("_driver", "", $driver_name); + + if($driver_name != null) + { + if(isset($this->_drivers[$driver_name])) + { + return $this->_drivers[$driver_name]; + } + else + { + rcube::raise_error("Unknown driver requested \"$driver_name\".", true, true); + } + } + else + { + if(!$quiet) { + rcube::raise_error("No driver name found in GPC.", true, true); + } + } + + return null; + } + + /** + * Helper function to retrieve the default driver + * + * @return mixed Driver object or null if no default driver could be determined. + */ + public function get_default_driver() + { + $default = $this->rc->config->get("calendar_driver_default", "kolab"); // Fallback to kolab if nothing was configured. + return $this->get_driver_by_name($default); + } + + /** + * Return the driver for the given event. + * + * @param $id ID or UID of the event. + * @return mixed Returns the driver object or null if no driver could be found for this event. + */ + public function get_driver_by_event($id) + { + foreach($this->get_drivers() as $driver) { + if($driver->get_event($id)) + return $driver; + } + + return null; + } + + /** + * Get driver for given calendar id. + * @param int Calendar id to get driver for. + * @return mixed Driver object for given calendar. + */ + public function get_driver_by_cal($cal_id) + { + if ($this->_cal_driver_map == null) + $this->get_calendars(); + + if (!isset($this->_cal_driver_map[$cal_id])){ + rcube::raise_error("No driver found for calendar \"$cal_id\".", true, true); + } + + return $this->_cal_driver_map[$cal_id]; + } + + /** + * Helper function to build calendar to driver map and calendar array. + * @return array List of calendar properties. + */ + public function get_calendars() + { + if ($this->_cals == null || $this->_cal_driver_map == null) { + $this->_cals = array(); + $this->_cal_driver_map = array(); + + $this->load_drivers(); + foreach ($this->get_drivers() as $driver) { + foreach ((array)$driver->list_calendars() as $id => $prop) { + $prop["driver"] = get_class($driver); + $this->_cals[$id] = $prop; + $this->_cal_driver_map[$id] = $driver; + } + } + } + + return $this->_cals; + } + + /** + * Load iTIP functions + */ + private function load_itip() + { + if (!$this->itip) { + require_once($this->home . '/lib/calendar_itip.php'); + $this->itip = new calendar_itip($this); + + if ($this->rc->config->get('kolab_invitation_calendars')) + $this->itip->set_rsvp_actions(array('accepted','tentative','declined','delegated','needs-action')); + } + + return $this->itip; + } + + /** + * Load iCalendar functions + */ + public function get_ical() + { + if (!$this->ical) { + $this->ical = libcalendaring::get_ical(); + } + + return $this->ical; + } + + /** + * Get properties of the calendar this user has specified as default + */ + public function get_default_calendar($sensitivity = null) + { + $default_id = $this->rc->config->get('calendar_default_calendar'); + // TODO: $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL | calendar_driver::FILTER_WRITEABLE); + + foreach($this->get_drivers() as $driver){ + $calendars = $driver->list_calendars(false, true); + if($default_id) { + $calendar = $calendars[$default_id] ?: null; + + if($calendar && (!$writeable || !$calendar["readonly"]) + && (!$confidential || $calendar["subtype"] != "confidential")) + { + //rcmail::console("422: get_default_calendar(): " . print_r($calendar, true)); + return $calendar; + } + } + else + { + // No default if, so get first calendar of first driver. + foreach ($calendars as $calendar) { + if ($calendar['default']) { + //rcmail::console("431: get_default_calendar(): " . print_r($calendar, true)); + return $calendar; + } + if ((!$writeable || !$calendar['readonly']) && (!$confidential || $calendar["subtype"] != "confidential")) { + //rcmail::console("435: get_default_calendar(): " . print_r($calendar, true)); + return $calendar; + } + } + } + } + + return null; + } + + /** + * Render the main calendar view from skin template + */ + function calendar_view() + { + $this->rc->output->set_pagetitle($this->gettext('calendar')); + + // Add CSS stylesheets to the page header + $this->ui->addCSS(); + + // Add JS files to the page header + $this->ui->addJS(); + + $this->ui->init_templates(); + $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close'); + $this->rc->output->add_label('libcalendaring.itipaccepted','libcalendaring.itiptentative','libcalendaring.itipdeclined','libcalendaring.itipdelegated','libcalendaring.expandattendeegroup','libcalendaring.expandattendeegroupnodata'); + + // initialize attendees autocompletion + rcube_autocomplete_init(); + + $this->rc->output->set_env('timezone', $this->timezone->getName()); + $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false); + $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver')); + $this->rc->output->set_env('mscolors', jqueryui::get_color_values()); + $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer')))); + + $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC); + if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) + $this->rc->output->set_env('view', $view); + + if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) + $this->rc->output->set_env('date', $date); + + if ($msgref = rcube_utils::get_input_value('itip', rcube_utils::INPUT_GPC)) + $this->rc->output->set_env('itip_events', $this->itip_events($msgref)); + + $this->rc->output->send("calendar.calendar"); + } + + /** + * Handler for preferences_sections_list hook. + * Adds Calendar settings sections into preferences sections list. + * + * @param array Original parameters + * @return array Modified parameters + */ + function preferences_sections_list($p) + { + $p['list']['calendar'] = array( + 'id' => 'calendar', 'section' => $this->gettext('calendar'), + ); + + return $p; + } + + /** + * Handler for preferences_list hook. + * Adds options blocks into Calendar settings sections in Preferences. + * + * @param array Original parameters + * @return array Modified parameters + */ + function preferences_list($p) + { + if ($p['section'] != 'calendar') { + return $p; + } + + $no_override = array_flip((array)$this->rc->config->get('dont_override')); + + $p['blocks']['view']['name'] = $this->gettext('mainoptions'); + + if (!isset($no_override['calendar_default_view'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_default_view'; + $select = new html_select(array('name' => '_default_view', 'id' => $field_id)); + $select->add($this->gettext('day'), "agendaDay"); + $select->add($this->gettext('week'), "agendaWeek"); + $select->add($this->gettext('month'), "month"); + $select->add($this->gettext('agenda'), "table"); + $p['blocks']['view']['options']['default_view'] = array( + 'title' => html::label($field_id, Q($this->gettext('default_view'))), + 'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])), + ); + } + + if (!isset($no_override['calendar_timeslots'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_timeslot'; + $choices = array('1', '2', '3', '4', '6'); + $select = new html_select(array('name' => '_timeslots', 'id' => $field_id)); + $select->add($choices); + $p['blocks']['view']['options']['timeslots'] = array( + 'title' => html::label($field_id, Q($this->gettext('timeslots'))), + 'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))), + ); + } + + if (!isset($no_override['calendar_first_day'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_firstday'; + $select = new html_select(array('name' => '_first_day', 'id' => $field_id)); + $select->add(rcube_label('sunday'), '0'); + $select->add(rcube_label('monday'), '1'); + $select->add(rcube_label('tuesday'), '2'); + $select->add(rcube_label('wednesday'), '3'); + $select->add(rcube_label('thursday'), '4'); + $select->add(rcube_label('friday'), '5'); + $select->add(rcube_label('saturday'), '6'); + $p['blocks']['view']['options']['first_day'] = array( + 'title' => html::label($field_id, Q($this->gettext('first_day'))), + 'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))), + ); + } + + if (!isset($no_override['calendar_first_hour'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']))); + $select_hours = new html_select(); + for ($h = 0; $h < 24; $h++) + $select_hours->add(date($time_format, mktime($h, 0, 0)), $h); + + $field_id = 'rcmfd_firsthour'; + $p['blocks']['view']['options']['first_hour'] = array( + 'title' => html::label($field_id, Q($this->gettext('first_hour'))), + 'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)), + ); + } + + if (!isset($no_override['calendar_work_start'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_workstart'; + $p['blocks']['view']['options']['workinghours'] = array( + 'title' => html::label($field_id, Q($this->gettext('workinghours'))), + 'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) . + ' — ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)), + ); + } + + if (!isset($no_override['calendar_event_coloring'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_coloring'; + $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id)); + $select_colors->add($this->gettext('coloringmode0'), 0); + $select_colors->add($this->gettext('coloringmode1'), 1); + $select_colors->add($this->gettext('coloringmode2'), 2); + $select_colors->add($this->gettext('coloringmode3'), 3); + + $p['blocks']['view']['options']['eventcolors'] = array( + 'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))), + 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])), + ); + } + + if (!isset($no_override['calendar_default_alarm_type'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_alarm'; + $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id)); + $select_type->add($this->gettext('none'), ''); + $types = array(); + foreach ($this->get_drivers() as $driver) { + foreach ($driver->alarm_types as $type) { + $types[$type] = $type; + } + } + foreach ($types as $type) { + $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type); + } + $p['blocks']['view']['options']['alarmtype'] = array( + 'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))), + 'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')), + ); + } + + if (!isset($no_override['calendar_default_alarm_offset'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_alarm'; + $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3)); + $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset')); + foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) + $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger); + + $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); + $p['blocks']['view']['options']['alarmoffset'] = array( + 'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))), + 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), + ); + } + + if (!isset($no_override['calendar_default_calendar'])) { + if (!$p['current']) { + $p['blocks']['view']['content'] = true; + return $p; + } + // default calendar selection + $field_id = 'rcmfd_default_calendar'; + $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true)); + foreach($this->get_drivers() as $driver){ + foreach ((array)$driver->list_calendars(false, true) as $id => $prop) { + $select_cal->add($prop['name'], strval($id)); + if ($prop['default']) + $default_calendar = $id; + } + } + $p['blocks']['view']['options']['defaultcalendar'] = array( + 'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))), + 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)), + ); + } + + $p['blocks']['itip']['name'] = $this->gettext('itipoptions'); + + // Invitations handling + if (!isset($no_override['calendar_itip_after_action'])) { + if (!$p['current']) { + $p['blocks']['itip']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_after_action'; + $select = new html_select(array('name' => '_after_action', 'id' => $field_id, + 'onchange' => "\$('#{$field_id}_select')[this.value == 4 ? 'show' : 'hide']()")); + + $select->add($this->gettext('afternothing'), ''); + $select->add($this->gettext('aftertrash'), 1); + $select->add($this->gettext('afterdelete'), 2); + $select->add($this->gettext('afterflagdeleted'), 3); + $select->add($this->gettext('aftermoveto'), 4); + + $val = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']); + if ($val !== null && $val !== '' && !is_int($val)) { + $folder = $val; + $val = 4; + } + + $folders = $this->rc->folder_selector(array( + 'id' => $field_id . '_select', + 'name' => '_after_action_folder', + 'maxlength' => 30, + 'folder_filter' => 'mail', + 'folder_rights' => 'w', + 'style' => $val !== 4 ? 'display:none' : '', + )); + + $p['blocks']['itip']['options']['after_action'] = array( + 'title' => html::label($field_id, Q($this->gettext('afteraction'))), + 'content' => $select->show($val) . $folders->show($folder), + ); + } + + // category definitions + foreach ($this->get_drivers() as $driver) { + if (!$driver->nocategories && !isset($no_override['calendar_categories'])) { + $p['blocks']['categories']['name'] = $this->gettext('categories'); + + if (!$p['current']) { + $p['blocks']['categories']['content'] = true; + return $p; + } + + $categories = (array)$driver->list_categories(); + $categories_list = ''; + foreach ($categories as $name => $color) { + $key = md5($name); + $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name); + $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category'))); + $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $driver->categoriesimmutable)); + $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6)); + $hidden = $driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : ''; + $categories_list .= html::div(null, $hidden . $category_name->show($name) . ' ' . $category_color->show($color) . ' ' . $category_remove->show()); + } + + $p['blocks']['categories']['options']['category_' . $name] = array( + 'content' => html::div(array('id' => 'calendarcategories'), $categories_list), + ); + + $field_id = 'rcmfd_new_category'; + $new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30)); + $add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()")); + $p['blocks']['categories']['options']['categories'] = array( + 'content' => $new_category->show('') . ' ' . $add_category->show(), + ); + + $this->rc->output->add_script('function rcube_calendar_add_category(){ + var name = $("#rcmfd_new_category").val(); + if (name.length) { + var input = $("").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name); + var color = $("").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000"); + var button = $("").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() }); + $("
").append(input).append(" ").append(color).append(" ").append(button).appendTo("#calendarcategories"); + color.miniColors({ colorValues:(rcmail.env.mscolors || []) }); + $("#rcmfd_new_category").val(""); + } + }'); + + $this->rc->output->add_script('$("#rcmfd_new_category").keypress(function(event){ + if (event.which == 13) { + rcube_calendar_add_category(); + event.preventDefault(); + } + }); + ', 'docready'); + + // load miniColors js/css files + jqueryui::miniColors(); + } + } + + // virtual birthdays calendar + if (!isset($no_override['calendar_contact_birthdays'])) { + $p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar'); + + if (!$p['current']) { + $p['blocks']['birthdays']['content'] = true; + return $p; + } + + $field_id = 'rcmfd_contact_birthdays'; + $input = new html_checkbox(array('name' => '_contact_birthdays', 'id' => $field_id, 'value' => 1, 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)')); + + $p['blocks']['birthdays']['options']['contact_birthdays'] = array( + 'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')), + 'content' => $input->show($this->rc->config->get('calendar_contact_birthdays')?1:0), + ); + + $input_attrib = array( + 'class' => 'calendar_birthday_props', + 'disabled' => !$this->rc->config->get('calendar_contact_birthdays'), + ); + + $sources = array(); + $checkbox = new html_checkbox(array('name' => '_birthday_adressbooks[]') + $input_attrib); + foreach ($this->rc->get_address_sources(false, true) as $source) { + $active = in_array($source['id'], (array)$this->rc->config->get('calendar_birthday_adressbooks', array())) ? $source['id'] : ''; + $sources[] = html::label(null, $checkbox->show($active, array('value' => $source['id'])) . ' ' . rcube::Q($source['realname'] ?: $source['name'])); + } + + $p['blocks']['birthdays']['options']['birthday_adressbooks'] = array( + 'title' => rcube::Q($this->gettext('birthdayscalendarsources')), + 'content' => join(html::br(), $sources), + ); + + $field_id = 'rcmfd_birthdays_alarm'; + $select_type = new html_select(array('name' => '_birthdays_alarm_type', 'id' => $field_id) + $input_attrib); + $select_type->add($this->gettext('none'), ''); + foreach ($this->get_default_driver()->alarm_types as $type) { // TODO: Replace with dedicated birthday calendar as soon as it is available + $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type); + } + + $input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib); + $select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib); + foreach (array('-M','-H','-D') as $trigger) + $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger); + + $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D')); + $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array( + 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))), + 'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]), + ); + } + + return $p; + } + + /** + * Handler for preferences_save hook. + * Executed on Calendar settings form submit. + * + * @param array Original parameters + * @return array Modified parameters + */ + function preferences_save($p) + { + if ($p['section'] == 'calendar') { + + // compose default alarm preset value + $alarm_offset = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST); + $alarm_value = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST); + $default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1]; + + $birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST); + $birthdays_alarm_value = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST); + $birthdays_alarm_value = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1]; + + $p['prefs'] = array( + 'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST), + 'calendar_timeslots' => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)), + 'calendar_first_day' => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)), + 'calendar_first_hour' => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)), + 'calendar_work_start' => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)), + 'calendar_work_end' => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)), + 'calendar_event_coloring' => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)), + 'calendar_default_alarm_type' => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST), + 'calendar_default_alarm_offset' => $default_alarm, + 'calendar_default_calendar' => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST), + 'calendar_date_format' => null, // clear previously saved values + 'calendar_time_format' => null, + 'calendar_contact_birthdays' => rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST) ? true : false, + 'calendar_birthday_adressbooks' => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST), + 'calendar_birthdays_alarm_type' => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST), + 'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null, + 'calendar_itip_after_action' => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)), + ); + + if ($p['prefs']['calendar_itip_after_action'] == 4) { + $p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true); + } + + // categories + foreach($this->get_drivers() as $driver) { + if (!$driver->nocategories) { + $old_categories = $new_categories = array(); + foreach ($driver->list_categories() as $name => $color) { + $old_categories[md5($name)] = $name; + } + + $categories = (array)rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST); + $colors = (array)rcube_utils::get_input_value('_colors', rcube_utils::INPUT_POST); + + foreach ($categories as $key => $name) { + $color = preg_replace('/^#/', '', strval($colors[$key])); + + // rename categories in existing events -> driver's job + if ($oldname = $old_categories[$key]) { + $driver->replace_category($oldname, $name, $color); + unset($old_categories[$key]); + } else + $driver->add_category($name, $color); + + $new_categories[$name] = $color; + } + + // these old categories have been removed, alter events accordingly -> driver's job + foreach ((array)$old_categories[$key] as $key => $name) { + $driver->remove_category($name); + } + + $p['prefs']['calendar_categories'] = $new_categories; + } + } + } + + return $p; + } + + /** + * Dispatcher for calendar actions initiated by the client + */ + function calendar_action() + { + $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); + $cal = rcube_utils::get_input_value('c', rcube_utils::INPUT_GPC); + $success = $reload = false; + $driver = null; + + if (isset($cal['showalarms'])) + $cal['showalarms'] = intval($cal['showalarms']); + + switch ($action) { + case "form-new": + case "form-edit": + echo $this->ui->calendar_editform($action, $cal); + exit; + case "new": + $driver = $this->get_driver_by_gpc(); + $success = $driver->create_calendar($cal); + $reload = true; + break; + case "edit": + $driver = $this->get_driver_by_cal($cal['id']); + $success = $driver->edit_calendar($cal); + $reload = true; + break; + case "delete": + $driver = $this->get_driver_by_cal($cal['id']); + if ($success = $driver->delete_calendar($cal)) + $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id'])); + break; + case "subscribe": + $driver = $this->get_driver_by_cal($cal['id']); + if (!$driver->subscribe_calendar($cal)) + $this->rc->output->show_message($this->gettext('errorsaving'), 'error'); + return; + case "search": + $results = array(); + $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); + $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); + $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); + + $search_more_results = false; + foreach($this->get_drivers() as $driver) { + foreach ((array)$driver->search_calendars($query, $source) as $id => $prop) { + $editname = $prop['editname']; + unset($prop['editname']); // force full name to be displayed + $prop['active'] = false; + + // let the UI generate HTML and CSS representation for this calendar + $html = $this->ui->calendar_list_item($id, $prop, $jsenv); + $cal = $jsenv[$id]; + $cal['editname'] = $editname; + $cal['html'] = $html; + if (!empty($prop['color'])) + $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode); + + $results[] = $cal; + } + + $search_more_results |= $driver->search_more_results; + } + + // report more results available + if ($search_more_results) + $this->rc->output->show_message('autocompletemore', 'info'); + + $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC)); + return; + } + + if ($success) + $this->rc->output->show_message('successfullysaved', 'confirmation'); + else { + $error_msg = $this->gettext('errorsaving') . ($driver && $driver->last_error ? ': ' . $driver->last_error :''); + $this->rc->output->show_message($error_msg, 'error'); + } + + $this->rc->output->command('plugin.unlock_saving'); + + if ($success && $reload) + $this->rc->output->command('plugin.reload_view'); + } + + + /** + * Dispatcher for event actions initiated by the client + */ + function event_action() + { + $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); + $event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true); + $success = $reload = $got_msg = false; + + $driver = null; + if($event['calendar']) + $driver = $this->get_driver_by_cal($event['calendar']); + + // This can happen if creating a new event outside the calendar e.g. from an ical file attached to an email. + if(!$driver) + $driver = $this->get_default_driver(); + + // force notify if hidden + active + if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1) + $event['_notify'] = 1; + + // read old event data in order to find changes + if (($event['_notify'] || $event['_decline']) && $action != 'new') { + $old = $this->driver->get_event($event); + + // Support event moving across different drivers + if(isset($event["_fromcalendar"]) && $event["_fromcalendar"] != $event["calendar"]) { + $fromdriver = $this->get_driver_by_cal($event["_fromcalendar"]); + if(get_class($fromdriver) != get_class($driver)) { + $fromevent = $event; + $fromevent["calendar"] = $event["_fromcalendar"]; + if($fromdriver->remove_event($fromevent)) + $action = "new"; + } + } + + // load main event if savemode is 'all' or if deleting 'future' events + if (($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && !$event['_decline'])) && $old['recurrence_id']) { + $old['id'] = $old['recurrence_id']; + $old = $this->driver->get_event($old); + } + } + + switch ($action) { + case "new": + // create UID for new event + $event['uid'] = $this->generate_uid(); + $this->write_preprocess($event, $action); + if ($success = $driver->new_event($event)) { + $event['id'] = $event['uid']; + $event['_savemode'] = 'all'; + $this->cleanup_event($event); + $this->event_save_success($event, null, $action, true); + } + $reload = $success && $event['recurrence'] ? 2 : 1; + break; + + case "edit": + $this->write_preprocess($event, $action); + if ($success = $driver->edit_event($event)) { + $this->cleanup_event($event); + $this->event_save_success($event, $old, $action, $success); + } + $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1; + break; + + case "resize": + $this->write_preprocess($event, $action); + if ($success = $driver->resize_event($event)) { + $this->event_save_success($event, $old, $action, $success); + } + $reload = $event['_savemode'] ? 2 : 1; + break; + + case "move": + $this->write_preprocess($event, $action); + if ($success = $driver->move_event($event)) { + $this->event_save_success($event, $old, $action, $success); + } + $reload = $success && $event['_savemode'] ? 2 : 1; + break; + + case "remove": + // remove previous deletes + $undo_time = $driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0; + $this->rc->session->remove('calendar_event_undo'); + + // search for event if only UID is given + if (!isset($event['calendar']) && $event['uid']) { + if (!($event = $driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) { + break; + } + $undo_time = 0; + } + + $success = $driver->remove_event($event, $undo_time < 1); + $reload = (!$success || $event['_savemode']) ? 2 : 1; + + if ($undo_time > 0 && $success) { + $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event); + // display message with Undo link. + $msg = html::span(null, $this->gettext('successremoval')) + . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))", + JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo')); + $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time); + $got_msg = true; + } + else if ($success) { + $this->rc->output->show_message('calendar.successremoval', 'confirmation'); + $got_msg = true; + } + + // send cancellation for the main event + if ($event['_savemode'] == 'all') { + unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']); + } + // send an update for the main event's recurrence rule instead of a cancellation message + else if ($event['_savemode'] == 'future' && $success !== false && $success !== true) { + $event['_savemode'] = 'all'; // force event_save_success() to load master event + $action = 'edit'; + $success = true; + } + + // send iTIP reply that participant has declined the event + if ($success && $event['_decline']) { + $emails = $this->get_user_emails(); + foreach ($old['attendees'] as $i => $attendee) { + if ($attendee['role'] == 'ORGANIZER') + $organizer = $attendee; + else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + $old['attendees'][$i]['status'] = 'DECLINED'; + $reply_sender = $attendee['email']; + } + } + + if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) { + $old['thisandfuture'] = true; + } + + $itip = $this->load_itip(); + $itip->set_sender_email($reply_sender); + if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined')) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); + else + $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); + } + else if ($success) { + $this->event_save_success($event, $old, $action, $success); + } + break; + + case "undo": + // Restore deleted event + $event = $_SESSION['calendar_event_undo']['data']; + + if ($event) + $success = $driver->restore_event($event); + + if ($success) { + $this->rc->session->remove('calendar_event_undo'); + $this->rc->output->show_message('calendar.successrestore', 'confirmation'); + $got_msg = true; + $reload = 2; + } + + break; + + case "rsvp": + $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); + $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST); + $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST); + $reply_comment = $event['comment']; + + $this->write_preprocess($event, 'edit'); + $ev = $driver->get_event($event); + $ev['attendees'] = $event['attendees']; + $ev['free_busy'] = $event['free_busy']; + $ev['_savemode'] = $event['_savemode']; + + // send invitation to delegatee + add it as attendee + if ($status == 'delegated' && $event['to']) { + $itip = $this->load_itip(); + if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) { + $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); + $noreply = false; + } + } + + $event = $ev; + + // compose a list of attendees affected by this change + $updated_attendees = array_filter(array_map(function($j) use ($event) { + return $event['attendees'][$j]; + }, $attendees)); + + if ($success = $driver->edit_rsvp($event, $status, $updated_attendees)) { + $noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC); + $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0; + $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1; + $organizer = null; + $emails = $this->get_user_emails(); + + foreach ($event['attendees'] as $i => $attendee) { + if ($attendee['role'] == 'ORGANIZER') { + $organizer = $attendee; + } + else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + $reply_sender = $attendee['email']; + } + } + + if (!$noreply) { + $itip = $this->load_itip(); + $itip->set_sender_email($reply_sender); + $event['comment'] = $reply_comment; + $event['thisandfuture'] = $event['_savemode'] == 'future'; + if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); + else + $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); + } + + // refresh all calendars + if ($event['calendar'] != $ev['calendar']) { + $this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true)); + $reload = 0; + } + } + break; + + case "dismiss": + $event['ids'] = explode(',', $event['id']); + $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event); + $success = $plugin['success']; + foreach ($event['ids'] as $id) { + if (strpos($id, 'cal:') === 0) + $success |= $driver->dismiss_alarm(substr($id, 4), $event['snooze']); + } + break; + + case "changelog": + $data = $driver->get_event_changelog($event); + if (is_array($data) && !empty($data)) { + $lib = $this->lib; + $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); + array_walk($data, function(&$change) use ($lib, $dtformat) { + if ($change['date']) { + $dt = $lib->adjust_timezone($change['date']); + if ($dt instanceof DateTime) + $change['date'] = $this->rc->format_date($dt, $dtformat, false); + } + }); + $this->rc->output->command('plugin.render_event_changelog', $data); + } + else { + $this->rc->output->command('plugin.render_event_changelog', false); + } + $got_msg = true; + $reload = false; + break; + + case "diff": + $data = $driver->get_event_diff($event, $event['rev1'], $event['rev2']); + if (is_array($data)) { + // convert some properties, similar to self::_client_event() + $lib = $this->lib; + array_walk($data['changes'], function(&$change, $i) use ($event, $lib) { + // convert date cols + foreach (array('start','end','created','changed') as $col) { + if ($change['property'] == $col) { + $change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c'); + $change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c'); + } + } + // create textual representation for alarms and recurrence + if ($change['property'] == 'alarms') { + if (is_array($change['old'])) + $change['old_'] = libcalendaring::alarm_text($change['old']); + if (is_array($change['new'])) + $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new'])); + } + if ($change['property'] == 'recurrence') { + if (is_array($change['old'])) + $change['old_'] = $lib->recurrence_text($change['old']); + if (is_array($change['new'])) + $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new'])); + } + if ($change['property'] == 'attachments') { + if (is_array($change['old'])) + $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']); + if (is_array($change['new'])) + $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']); + } + // compute a nice diff of description texts + if ($change['property'] == 'description') { + $change['diff_'] = libkolab::html_diff($change['old'], $change['new']); + } + }); + $this->rc->output->command('plugin.event_show_diff', $data); + } + else { + $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); + } + $got_msg = true; + $reload = false; + break; + + case "show": + if ($event = $driver->get_event_revison($event, $event['rev'])) { + $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event)); + } + else { + $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error'); + } + $got_msg = true; + $reload = false; + break; + + case "restore": + if ($success = $driver->restore_event_revision($event, $event['rev'])) { + $_event = $driver->get_event($event); + $reload = $_event['recurrence'] ? 2 : 1; + $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation'); + $this->rc->output->command('plugin.close_history_dialog'); + } + else { + $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error'); + $reload = 0; + } + $got_msg = true; + break; + } + + // show confirmation/error message + if (!$got_msg) { + if ($success) + $this->rc->output->show_message('successfullysaved', 'confirmation'); + else + $this->rc->output->show_message('calendar.errorsaving', 'error'); + } + + // unlock client + $this->rc->output->command('plugin.unlock_saving'); + + // update event object on the client or trigger a complete refretch if too complicated + if ($reload) { + $args = array('source' => $event['calendar']); + if ($reload > 1) + $args['refetch'] = true; + else if ($success && $action != 'remove') + $args['update'] = $this->_client_event($driver->get_event($event), true); + $this->rc->output->command('plugin.refresh_calendar', $args); + } + } + + /** + * Helper method sending iTip notifications after successful event updates + */ + private function event_save_success(&$event, $old, $action, $success) + { + // $success is a new event ID + if ($success !== true) { + // send update notification on the main event + if ($event['_savemode'] == 'future' && $event['_notify'] && $old['attendees'] && $old['recurrence_id']) { + $master = $this->driver->get_event(array('id' => $old['recurrence_id'], 'calendar' => $old['calendar']), 0, true); + unset($master['_instance'], $master['recurrence_date']); + + $sent = $this->notify_attendees($master, null, $action, $event['_comment']); + if ($sent < 0) + $this->rc->output->show_message('calendar.errornotifying', 'error'); + + $event['attendees'] = $master['attendees']; // this tricks us into the next if clause + } + + // delete old reference if saved as new + if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') { + $old = null; + } + + $event['id'] = $success; + $event['_savemode'] = 'all'; + } + + // send out notifications + if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) { + $_savemode = $event['_savemode']; + + // send notification for the main event when savemode is 'all' + if ($action != 'remove' && $_savemode == 'all' && ($event['recurrence_id'] || $old['recurrence_id'] || ($old && $old['id'] != $event['id']))) { + $event['id'] = $event['recurrence_id'] ?: ($old['recurrence_id'] ?: $old['id']); + $event = $this->driver->get_event($event, 0, true); + unset($event['_instance'], $event['recurrence_date']); + } + else { + // make sure we have the complete record + $event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true); + } + + $event['_savemode'] = $_savemode; + + if ($old) { + $old['thisandfuture'] = $_savemode == 'future'; + } + + // only notify if data really changed (TODO: do diff check on client already) + if (!$old || $action == 'remove' || self::event_diff($event, $old)) { + $sent = $this->notify_attendees($event, $old, $action, $event['_comment']); + if ($sent > 0) + $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); + else if ($sent < 0) + $this->rc->output->show_message('calendar.errornotifying', 'error'); + } + } + } + + /** + * Handler for load-requests from fullcalendar + * This will return pure JSON formatted output + */ + function load_events() + { + $events = $this->get_driver_by_gpc()->load_events( + rcube_utils::get_input_value('start', rcube_utils::INPUT_GET), + rcube_utils::get_input_value('end', rcube_utils::INPUT_GET), + ($query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET)), + rcube_utils::get_input_value('source', rcube_utils::INPUT_GET) + ); + echo $this->encode($events, !empty($query)); + exit; + } + + /** + * Handler for requests fetching event counts for calendars + */ + public function count_events() + { + // don't update session on these requests (avoiding race conditions) + $this->rc->session->nowrite = true; + + $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET); + if (!$start) { + $start = new DateTime('today 00:00:00', $this->timezone); + $start = $start->format('U'); + } + + $counts = 0; + foreach($this->get_drivers() as $driver) { + $counts += $driver->count_events( + rcube_utils::get_input_value('source', rcube_utils::INPUT_GET), + $start, + rcube_utils::get_input_value('end', rcube_utils::INPUT_GET) + ); + } + + $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); + } + + /** + * Load event data from an iTip message attachment + */ + public function itip_events($msgref) + { + $path = explode('/', $msgref); + $msg = array_pop($path); + $mbox = join('/', $path); + list($uid, $mime_id) = explode('#', $msg); + $events = array(); + + if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { + $partstat = 'NEEDS-ACTION'; +/* + $user_emails = $this->lib->get_user_emails(); + foreach ($event['attendees'] as $attendee) { + if (in_array($attendee['email'], $user_emails)) { + $partstat = $attendee['status']; + break; + } + } +*/ + $event['id'] = $event['uid']; + $event['temporary'] = true; + $event['readonly'] = true; + $event['calendar'] = '--invitation--itip'; + $event['className'] = 'fc-invitation-' . strtolower($partstat); + $event['_mbox'] = $mbox; + $event['_uid'] = $uid; + $event['_part'] = $mime_id; + + $events[] = $this->_client_event($event, true); + + // add recurring instances + if (!empty($event['recurrence'])) { + foreach ($this->driver->get_recurring_events($event, $event['start']) as $recurring) { + $recurring['temporary'] = true; + $recurring['readonly'] = true; + $recurring['calendar'] = '--invitation--itip'; + $events[] = $this->_client_event($recurring, true); + } + } + } + + return $events; + } + + /** + * Handler for keep-alive requests + * This will check for updated data in active calendars and sync them to the client + */ + public function refresh($attr) + { + // refresh the entire calendar every 10th time to also sync deleted events + if (rand(0,10) == 10) { + $this->rc->output->command('plugin.refresh_calendar', array('refetch' => true)); + return; + } + + $counts = array(); + + foreach($this->get_drivers() as $driver) { + foreach ($driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) { + $events = $driver->load_events( + rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC), + rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC), + rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), + $cal['id'], + 1, + $attr['last'] + ); + + foreach ($events as $event) { + $this->rc->output->command('plugin.refresh_calendar', + array('source' => $cal['id'], 'update' => $this->_client_event($event))); + } + + // refresh count for this calendar + if ($cal['counts']) { + $today = new DateTime('today 00:00:00', $this->timezone); + $counts += $driver->count_events($cal['id'], $today->format('U')); + } + } + } + + if (!empty($counts)) { + $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); + } + } + + /** + * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. + * This will check for pending notifications and pass them to the client + */ + public function pending_alarms($p) + { + $time = $p['time'] ?: time(); + foreach($this->get_drivers() as $driver) { + if ($alarms = $driver->pending_alarms($time)) { + foreach ($alarms as $alarm) { + $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal: + $p['alarms'][] = $alarm; + } + } + } + + // get alarms for birthdays calendar + if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') { + $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db'); + + // TODO: Use dedicated birthday calendar as soon as it exists + foreach ($this->get_default_driver()->load_birthday_events($time, $time + 86400 * 60) as $e) { + $alarm = libcalendaring::get_next_alarm($e); + + // overwrite alarm time with snooze value (or null if dismissed) + if ($dismissed = $cache->get($e['id'])) + $alarm['time'] = $dismissed['notifyat']; + + // add to list if alarm is set + if ($alarm && $alarm['time'] && $alarm['time'] <= $time) { + $e['id'] = 'cal:bday:' . $e['id']; + $e['notifyat'] = $alarm['time']; + $p['alarms'][] = $e; + } + } + } + + return $p; + } + + /** + * Handler for alarm dismiss hook triggered by libcalendaring + */ + public function dismiss_alarms($p) + { + foreach($this->get_drivers() as $driver) { // TODO: Maybe use get_driver_by_cal() ? + foreach ((array)$p['ids'] as $id) { + if (strpos($id, 'cal:bday:') === 0) { + $p['success'] |= $driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']); + } else if (strpos($id, 'cal:') === 0) { + $p['success'] |= $driver->dismiss_alarm(substr($id, 4), $p['snooze']); + } + } + } + + return $p; + } + + /** + * Handler for check-recent requests which are accidentally sent to calendar taks + */ + function check_recent() + { + // NOP + $this->rc->output->send(); + } + + /** + * Hook triggered when a contact is saved + */ + function contact_update($p) + { + // clear birthdays calendar cache + if (!empty($p['record']['birthday'])) { + $cache = $this->rc->get_cache('calendar.birthdays', 'db'); + $cache->remove(); + } + } + + /** + * + */ + function import_events() + { + // Upload progress update + if (!empty($_GET['_progress'])) { + rcube_upload_progress(); + } + + @set_time_limit(0); + + // process uploaded file if there is no error + $err = $_FILES['_data']['error']; + + if (!$err && $_FILES['_data']['tmp_name']) { + $calendar = rcube_utils::get_input_value('calendar', rcube_utils::INPUT_GPC); + $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0; + + // extract zip file + if ($_FILES['_data']['type'] == 'application/zip') { + $count = 0; + if (class_exists('ZipArchive', false)) { + $zip = new ZipArchive(); + if ($zip->open($_FILES['_data']['tmp_name'])) { + $randname = uniqid('zip-' . session_id(), true); + $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname; + mkdir($tmpdir, 0700); + + // extract each ical file from the archive and import it + for ($i = 0; $i < $zip->numFiles; $i++) { + $filename = $zip->getNameIndex($i); + if (preg_match('/\.ics$/i', $filename)) { + $tmpfile = $tmpdir . '/' . basename($filename); + if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) { + $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors); + unlink($tmpfile); + } + } + } + + rmdir($tmpdir); + $zip->close(); + } + else { + $errors = 1; + $msg = 'Failed to open zip file.'; + } + } + else { + $errors = 1; + $msg = 'Zip files are not supported for import.'; + } + } + else { + // attempt to import teh uploaded file directly + $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors); + } + + if ($count) { + $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation'); + $this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true)); + } + else if (!$errors) { + $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice'); + $this->rc->output->command('plugin.import_success', array('source' => $calendar)); + } + else { + $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : ''))); + } + } + else { + if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { + $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array( + 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); + } + else { + $msg = rcube_label('fileuploaderror'); + } + + $this->rc->output->command('plugin.import_error', array('message' => $msg)); + } + + $this->rc->output->send('iframe'); + } + + /** + * Helper function to parse and import a single .ics file + */ + private function import_from_file($filepath, $calendar, $rangestart, &$errors) + { + $user_email = $this->rc->user->get_username(); + + $ical = $this->get_ical(); + $errors = !$ical->fopen($filepath); + $count = $i = 0; + $driver = $this->get_driver_by_cal($calendar); + foreach ($ical as $event) { + // keep the browser connection alive on long import jobs + if (++$i > 100 && $i % 100 == 0) { + echo ""; + ob_flush(); + } + + // TODO: correctly handle recurring events which start before $rangestart + if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart))) + continue; + + $event['_owner'] = $user_email; + $event['calendar'] = $calendar; + if ($driver->new_event($event)) { + $count++; + } + else { + $errors++; + } + } + + return $count; + } + + /** + * Construct the ics file for exporting events to iCalendar format; + */ + function export_events($terminate = true) + { + $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET); + $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET); + + if (!isset($start)) + $start = 'today -1 year'; + if (!is_numeric($start)) + $start = strtotime($start . ' 00:00:00'); + if (!$end) + $end = 'today +10 years'; + if (!is_numeric($end)) + $end = strtotime($end . ' 23:59:59'); + + $event_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET); + $attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET); + $calid = $filename = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET); + $driver = $this->get_driver_by_cal($calid); + $calendars = $this->driver->list_calendars(); + $events = array(); + + if ($calendars[$calid]) { + $filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid; + $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii + if (!empty($event_id)) { + if ($event = $driver->get_event(array('calendar' => $calid, 'id' => $event_id), 0, true)) { + if ($event['recurrence_id']) { + $event = $driver->get_event(array('calendar' => $calid, 'id' => $event['recurrence_id']), 0, true); + } + $events = array($event); + $filename = asciiwords($event['title']); + if (empty($filename)) + $filename = 'event'; + } + } + else { + $events = $driver->load_events($start, $end, null, $calid, 0); + if (empty($filename)) + $filename = $calid; + } + } + + header("Content-Type: text/calendar"); + header("Content-Disposition: inline; filename=".$filename.'.ics'); + + $this->get_ical()->export($events, '', true, $attachments ? array($driver, 'get_attachment_body') : null); + + if ($terminate) + exit; + } + + /** + * Handler for iCal feed requests + */ + function ical_feed_export() + { + $session_exists = !empty($_SESSION['user_id']); + + // process HTTP auth info + if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { + $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host() + $auth = $this->rc->plugins->exec_hook('authenticate', array( + 'host' => $this->rc->autoselect_host(), + 'user' => trim($_SERVER['PHP_AUTH_USER']), + 'pass' => $_SERVER['PHP_AUTH_PW'], + 'cookiecheck' => true, + 'valid' => true, + )); + if ($auth['valid'] && !$auth['abort']) + $this->rc->login($auth['user'], $auth['pass'], $auth['host']); + } + + // require HTTP auth + if (empty($_SESSION['user_id'])) { + header('WWW-Authenticate: Basic realm="Roundcube Calendar"'); + header('HTTP/1.0 401 Unauthorized'); + exit; + } + + // decode calendar feed hash + $format = 'ics'; + $calhash = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GET); + if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) { + $format = strtolower($m[1]); + $calhash = preg_replace($suff_regex, '', $calhash); + } + + if (!strpos($calhash, ':')) + $calhash = base64_decode($calhash); + + list($user, $_GET['source']) = explode(':', $calhash, 2); + + // sanity check user + if ($this->rc->user->get_username() == $user) { + $this->export_events(false); + } + else { + header('HTTP/1.0 404 Not Found'); + } + + // don't save session data + if (!$session_exists) + session_destroy(); + exit; + } + + + /** + * + */ + function load_settings() + { + $this->lib->load_settings(); + $this->defaults += $this->lib->defaults; + + $settings = array(); + + // configuration + $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar'); + $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']); + $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']); + + $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']); + $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); + $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']); + $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']); + $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']); + $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']); + $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']); + $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']); + $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']); + $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']); + $settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false); + $settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); + + // get user identity to create default attendee + if ($this->ui->screen == 'calendar') { + $identity = null; + foreach ($this->rc->user->list_emails() as $rec) { + if (!$identity) + $identity = $rec; + $identity['emails'][] = $rec['email']; + $settings['identities'][$rec['identity_id']] = $rec['email']; + } + $identity['emails'][] = $this->rc->user->get_username(); + $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails']))); + } + + return $settings; + } + + /** + * Encode events as JSON + * + * @param array Events as array + * @param boolean Add CSS class names according to calendar and categories + * @return string JSON encoded events + */ + function encode($events, $addcss = false) + { + $json = array(); + foreach ($events as $event) { + $json[] = $this->_client_event($event, $addcss); + } + return json_encode($json); + } + + /** + * Convert an event object to be used on the client + */ + private function _client_event($event, $addcss = false) + { + // compose a human readable strings for alarms_text and recurrence_text + if ($event['valarms']) { + $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']); + $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']); + } + if ($event['recurrence']) { + $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']); + $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']); + unset($event['recurrence_date']); + } + + foreach ((array)$event['attachments'] as $k => $attachment) { + $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); + } + + // Get driver for event calendar + $driver = $this->get_driver_by_cal($event['calendar']); + + // convert link URIs references into structs + if (array_key_exists('links', $event)) { + foreach ((array)$event['links'] as $i => $link) { + if (strpos($link, 'imap://') === 0 && ($msgref = $driver->get_message_reference($link))) { + $event['links'][$i] = $msgref; + } + } + } + + // check for organizer in attendees list + $organizer = null; + foreach ((array)$event['attendees'] as $i => $attendee) { + if ($attendee['role'] == 'ORGANIZER') { + $organizer = $attendee; + } + if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) { + $event['attendees'][$i]['noreply'] = true; + } + else { + unset($event['attendees'][$i]['noreply']); + } + } + + if ($organizer === null && !empty($event['organizer'])) { + $organizer = $event['organizer']; + $organizer['role'] = 'ORGANIZER'; + if (!is_array($event['attendees'])) + $event['attendees'] = array(); + array_unshift($event['attendees'], $organizer); + } + + // mapping url => vurl because of the fullcalendar client script + $event['vurl'] = $event['url']; + unset($event['url']); + + return array( + '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar + 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'), + 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'), + // 'changed' might be empty for event recurrences (Bug #2185) + 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null, + 'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null, + 'title' => strval($event['title']), + 'description' => strval($event['description']), + 'location' => strval($event['location']), + 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . + 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true) . + rtrim(' ' . $event['className']), + 'allDay' => ($event['allday'] == 1), + ) + $event; + } + + + /** + * Generate a unique identifier for an event + */ + public function generate_uid() + { + return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); + } + + + /** + * TEMPORARY: generate random event data for testing + * Create events by opening http:///?_task=calendar&_action=randomdata&_driver=kolab&_num=500&_date=2014-08-01&_dev=120 + */ + public function generate_randomdata() + { + @set_time_limit(0); + + $driver = $this->get_driver_by_gpc(); + $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100; + $date = $_REQUEST['_date'] ?: 'now'; + $dev = $_REQUEST['_dev'] ?: 30; + $cats = array_keys($driver->list_categories()); + $cals = $driver->list_calendars(calendar_driver::FILTER_ACTIVE); + $count = 0; + + while ($count++ < $num) { + $spread = intval($dev) * 86400; // days + $refdate = strtotime($date); + $start = round(($refdate + rand(-$spread, $spread)) / 600) * 600; + $duration = round(rand(30, 360) / 30) * 30 * 60; + $allday = rand(0,20) > 18; + $alarm = rand(-30,12) * 5; + $fb = rand(0,2); + + if (date('G', $start) > 23) + $start -= 3600; + + if ($allday) { + $start = strtotime(date('Y-m-d 00:00:00', $start)); + $duration = 86399; + } + + $title = ''; + $len = rand(2, 12); + $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise."); +// $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890"; + for ($i = 0; $i < $len; $i++) + $title .= $words[rand(0,count($words)-1)] . " "; + + $driver->new_event(array( + 'uid' => $this->generate_uid(), + 'start' => new DateTime('@'.$start), + 'end' => new DateTime('@'.($start + $duration)), + 'allday' => $allday, + 'title' => rtrim($title), + 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'), + 'categories' => $cats[array_rand($cats)], + 'calendar' => array_rand($cals), + 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '', + 'priority' => rand(0,9), + )); + } + + $this->rc->output->redirect(''); + } + + /** + * Handler for attachments upload + */ + public function attachment_upload() + { + $this->lib->attachment_upload(self::SESSION_KEY, 'cal-'); + } + + /** + * Handler for attachments download/displaying + */ + public function attachment_get() + { + // show loading page + if (!empty($_GET['_preload'])) { + return $this->lib->attachment_loading_page(); + } + + $event_id = rcube_utils::get_input_value('_event', rcube_utils::INPUT_GPC); + $calendar = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GPC); + $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); + $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC); + $driver = $this->get_driver_by_cal($calendar); + + $event = array('id' => $event_id, 'calendar' => $calendar, 'rev' => $rev); + $attachment = $driver->get_attachment($id, $event); + + // show part page + if (!empty($_GET['_frame'])) { + $this->lib->attachment = $attachment; + $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame')); + $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header')); + $this->rc->output->send('calendar.attachment'); + } + // deliver attachment content + else if ($attachment) { + $attachment['body'] = $driver->get_attachment_body($id, $event); + $this->lib->attachment_get($attachment); + } + + // if we arrive here, the requested part was not found + header('HTTP/1.1 404 Not Found'); + exit; + } + + + /** + * Prepares new/edited event properties before save + */ + private function write_preprocess(&$event, $action) + { + // convert dates into DateTime objects in user's current timezone + $event['start'] = new DateTime($event['start'], $this->timezone); + $event['end'] = new DateTime($event['end'], $this->timezone); + $event['allday'] = (bool)$event['allday']; + + // start/end is all we need for 'move' action (#1480) + if ($action == 'move') { + return; + } + + // convert the submitted recurrence settings + if (is_array($event['recurrence'])) { + $event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']); + } + + // convert the submitted alarm values + if ($event['valarms']) { + $event['valarms'] = libcalendaring::from_client_alarms($event['valarms']); + } + + $attachments = array(); + $eventid = 'cal-'.$event['id']; + + if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) { + if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) { + foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) { + if (is_array($event['attachments']) && in_array($id, $event['attachments'])) { + $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment); + } + } + } + } + + $event['attachments'] = $attachments; + + // convert link references into simple URIs + if (array_key_exists('links', $event)) { + $event['links'] = array_map(function($link) { + return is_array($link) ? $link['uri'] : strval($link); + }, (array)$event['links']); + } + + // check for organizer in attendees + if ($action == 'new' || $action == 'edit') { + if (!$event['attendees']) + $event['attendees'] = array(); + + $emails = $this->get_user_emails(); + $organizer = $owner = false; + foreach ((array)$event['attendees'] as $i => $attendee) { + if ($attendee['role'] == 'ORGANIZER') + $organizer = $i; + if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails)) + $owner = $i; + if (!isset($attendee['rsvp'])) + $event['attendees'][$i]['rsvp'] = true; + else if (is_string($attendee['rsvp'])) + $event['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1'; + } + + // set new organizer identity + if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc->user->get_identity($event['_identity']))) { + $event['attendees'][$organizer]['name'] = $identity['name']; + $event['attendees'][$organizer]['email'] = $identity['email']; + } + + // set owner as organizer if yet missing + if ($organizer === false && $owner !== false) { + $event['attendees'][$owner]['role'] = 'ORGANIZER'; + unset($event['attendees'][$owner]['rsvp']); + } + } + + // mapping url => vurl because of the fullcalendar client script + if (array_key_exists('vurl', $event)) { + $event['url'] = $event['vurl']; + unset($event['vurl']); + } + } + + /** + * Releases some resources after successful event save + */ + private function cleanup_event(&$event) + { + // remove temp. attachment files + if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) { + $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid)); + $this->rc->session->remove(self::SESSION_KEY); + } + } + + /** + * Send out an invitation/notification to all event attendees + */ + private function notify_attendees($event, $old, $action = 'edit', $comment = null) + { + if ($action == 'remove' || ($event['status'] == 'CANCELLED' && $old['status'] != $event['status'])) { + $event['cancelled'] = true; + $is_cancelled = true; + } + + $itip = $this->load_itip(); + $emails = $this->get_user_emails(); + $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); + + // add comment to the iTip attachment + $event['comment'] = $comment; + + // set a valid recurrence-id if this is a recurrence instance + libcalendaring::identify_recurrence_instance($event); + + // compose multipart message using PEAR:Mail_Mime + $method = $action == 'remove' ? 'CANCEL' : 'REQUEST'; + $message = $itip->compose_itip_message($event, $method, !$old || $event['sequence'] > $old['sequence']); + + // list existing attendees from $old event + $old_attendees = array(); + foreach ((array)$old['attendees'] as $attendee) { + $old_attendees[] = $attendee['email']; + } + + // send to every attendee + $sent = 0; $current = array(); + foreach ((array)$event['attendees'] as $attendee) { + $current[] = strtolower($attendee['email']); + + // skip myself for obvious reasons + if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) + continue; + + // skip if notification is disabled for this attendee + if ($attendee['noreply'] && $itip_notify & 2) + continue; + + // skip if this attendee has delegated and set RSVP=FALSE + if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) + continue; + + // which template to use for mail text + $is_new = !in_array($attendee['email'], $old_attendees); + $is_rsvp = $is_new || $event['sequence'] > $old['sequence']; + $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody'); + $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty')); + + $event['comment'] = $comment; + + // finally send the message + if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message, $is_rsvp)) + $sent++; + else + $sent = -100; + } + + // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions + + // send CANCEL message to removed attendees + foreach ((array)$old['attendees'] as $attendee) { + if ($attendee['role'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current)) + continue; + + $vevent = $old; + $vevent['cancelled'] = $is_cancelled; + $vevent['attendees'] = array($attendee); + $vevent['comment'] = $comment; + if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody')) + $sent++; + else + $sent = -100; + } + + return $sent; + } + + private function _get_freebusy_list($email, $start, $end) + { + $fblist = array(); + foreach($this->get_drivers() as $driver){ + if($driver->freebusy) { + $cur = $driver->get_freebusy_list($email, $start, $end); + if($cur) { + $fblist = array_merge($fblist, $cur); + } + } + } + + if(sizeof($fblist) == 0) return false; + else return $fblist; + } + + /** + * Echo simple free/busy status text for the given user and time range + */ + public function freebusy_status() + { + $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC); + $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC); + $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC); + + // convert dates into unix timestamps + if (!empty($start) && !is_numeric($start)) { + $dts = new DateTime($start, $this->timezone); + $start = $dts->format('U'); + } + if (!empty($end) && !is_numeric($end)) { + $dte = new DateTime($end, $this->timezone); + $end = $dte->format('U'); + } + + if (!$start) $start = time(); + if (!$end) $end = $start + 3600; + + $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE'); + $status = 'UNKNOWN'; + + // if the backend has free-busy information + $fblist = $this->_get_freebusy_list($email, $start, $end); + if (is_array($fblist)) { + $status = 'FREE'; + + foreach ($fblist as $slot) { + list($from, $to, $type) = $slot; + if ($from < $end && $to > $start) { + $status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY'; + break; + } + } + } + + // let this information be cached for 5min + send_future_expire_header(300); + + echo $status; + exit; + } + + /** + * Return a list of free/busy time slots within the given period + * Echo data in JSON encoding + */ + public function freebusy_times() + { + $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC); + $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC); + $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC); + $interval = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC)); + $strformat = $interval > 60 ? 'Ymd' : 'YmdHis'; + + // convert dates into unix timestamps + if (!empty($start) && !is_numeric($start)) { + $dts = rcube_utils::anytodatetime($start, $this->timezone); + $start = $dts ? $dts->format('U') : null; + } + if (!empty($end) && !is_numeric($end)) { + $dte = rcube_utils::anytodatetime($end, $this->timezone); + $end = $dte ? $dte->format('U') : null; + } + + if (!$start) $start = time(); + if (!$end) $end = $start + 86400 * 30; + if (!$interval) $interval = 60; // 1 hour + + if (!$dte) { + $dts = new DateTime('@'.$start); + $dts->setTimezone($this->timezone); + } + + $fblist = $this->_get_freebusy_list($email, $start, $end); + $slots = array(); + + // build a list from $start till $end with blocks representing the fb-status + for ($s = 0, $t = $start; $t <= $end; $s++) { + $status = self::FREEBUSY_UNKNOWN; + $t_end = $t + $interval * 60; + $dt = new DateTime('@'.$t); + $dt->setTimezone($this->timezone); + + // determine attendee's status + if (is_array($fblist)) { + $status = self::FREEBUSY_FREE; + foreach ($fblist as $slot) { + list($from, $to, $type) = $slot; + + // check for possible all-day times + if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') { + // shift into the user's timezone for sane matching + $from -= $this->gmt_offset; + $to -= $this->gmt_offset; + } + + if ($from < $t_end && $to > $t) { + $status = isset($type) ? $type : self::FREEBUSY_BUSY; + if ($status == self::FREEBUSY_BUSY) // can't get any worse :-) + break; + } + } + } + + $slots[$s] = $status; + $times[$s] = intval($dt->format($strformat)); + $t = $t_end; + } + + $dte = new DateTime('@'.$t_end); + $dte->setTimezone($this->timezone); + + // let this information be cached for 5min + send_future_expire_header(300); + + echo json_encode(array( + 'email' => $email, + 'start' => $dts->format('c'), + 'end' => $dte->format('c'), + 'interval' => $interval, + 'slots' => $slots, + 'times' => $times, + )); + exit; + } + + /** + * Handler for printing calendars + */ + public function print_view() + { + $title = $this->gettext('print'); + + $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC); + if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table'))) + $view = 'agendaDay'; + + $this->rc->output->set_env('view',$view); + + if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC)) + $this->rc->output->set_env('date', $date); + + if ($range = rcube_utils::get_input_value('range', rcube_utils::INPUT_GPC)) + $this->rc->output->set_env('listRange', intval($range)); + + if (isset($_REQUEST['sections'])) + $this->rc->output->set_env('listSections', rcube_utils::get_input_value('sections', rcube_utils::INPUT_GPC)); + + if ($search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC)) { + $this->rc->output->set_env('search', $search); + $title .= ' "' . $search . '"'; + } + + // Add CSS stylesheets to the page header + $skin_path = $this->local_skin_path(); + $this->include_stylesheet($skin_path . '/fullcalendar.css'); + $this->include_stylesheet($skin_path . '/print.css'); + + // Add JS files to the page header + $this->include_script('print.js'); + $this->include_script('lib/js/fullcalendar.js'); + + $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css')); + $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list')); + + $this->rc->output->set_pagetitle($title); + $this->rc->output->send("calendar.print"); + } + + /** + * + */ + public function get_inline_ui() + { + foreach (array('save','cancel','savingdata') as $label) + $texts['calendar.'.$label] = $this->gettext($label); + + $texts['calendar.new_event'] = $this->gettext('createfrommail'); + + $this->ui->init_templates(); + $this->ui->calendar_list(); # set env['calendars'] + echo $this->api->output->parse('calendar.eventedit', false, false); + echo html::tag('script', array('type' => 'text/javascript'), + "rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n". + "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n". + "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n". + "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n". + "rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n". + "rcmail.add_label(" . json_encode($texts) . ");\n" + ); + exit; + } + + /** + * Compare two event objects and return differing properties + * + * @param array Event A + * @param array Event B + * @return array List of differing event properties + */ + public static function event_diff($a, $b) + { + $diff = array(); + $ignore = array('changed' => 1, 'attachments' => 1); + foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { + if (!$ignore[$key] && $key[0] != '_' && $a[$key] != $b[$key]) + $diff[] = $key; + } + + // only compare number of attachments + if (count($a['attachments']) != count($b['attachments'])) + $diff[] = 'attachments'; + + return $diff; + } + + /** + * Update attendee properties on the given event object + * + * @param array The event object to be altered + * @param array List of hash arrays each represeting an updated/added attendee + */ + public static function merge_attendee_data(&$event, $attendees, $removed = null) + { + if (!empty($attendees) && !is_array($attendees[0])) { + $attendees = array($attendees); + } + + foreach ($attendees as $attendee) { + $found = false; + + foreach ($event['attendees'] as $i => $candidate) { + if ($candidate['email'] == $attendee['email']) { + $event['attendees'][$i] = $attendee; + $found = true; + break; + } + } + + if (!$found) { + $event['attendees'][] = $attendee; + } + } + + // filter out removed attendees + if (!empty($removed)) { + $event['attendees'] = array_filter($event['attendees'], function($attendee) use ($removed) { + return !in_array($attendee['email'], $removed); + }); + } + } + + + /**** Resource management functions ****/ + + /** + * Getter for the configured implementation of the resource directory interface + */ + private function resources_directory() + { + if (is_object($this->resources_dir)) { + return $this->resources_dir; + } + + if ($driver_name = $this->rc->config->get('calendar_resources_driver')) { + $driver_class = 'resources_driver_' . $driver_name; + + require_once($this->home . '/drivers/resources_driver.php'); + require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php'); + + $this->resources_dir = new $driver_class($this); + } + + return $this->resources_dir; + } + + /** + * Handler for resoruce autocompletion requests + */ + public function resources_autocomplete() + { + $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true); + $sid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC); + $maxnum = (int)$this->rc->config->get('autocomplete_max', 15); + $results = array(); + + if ($directory = $this->resources_directory()) { + foreach ($directory->load_resources($search, $maxnum) as $rec) { + $results[] = array( + 'name' => $rec['name'], + 'email' => $rec['email'], + 'type' => $rec['_type'], + ); + } + } + + $this->rc->output->command('ksearch_query_results', $results, $search, $sid); + $this->rc->output->send(); + } + + /** + * Handler for load-requests for resource data + */ + function resources_list() + { + $data = array(); + + if ($directory = $this->resources_directory()) { + foreach ($directory->load_resources() as $rec) { + $data[] = $rec; + } + } + + $this->rc->output->command('plugin.resource_data', $data); + $this->rc->output->send(); + } + + /** + * Handler for requests loading resource owner information + */ + function resources_owner() + { + if ($directory = $this->resources_directory()) { + $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); + $data = $directory->get_resource_owner($id); + } + + $this->rc->output->command('plugin.resource_owner', $data); + $this->rc->output->send(); + } + + /** + * Deliver event data for a resource's calendar + */ + function resources_calendar() + { + $events = array(); + + if ($directory = $this->resources_directory()) { + $events = $directory->get_resource_calendar( + rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC), + rcube_utils::get_input_value('start', rcube_utils::INPUT_GET), + rcube_utils::get_input_value('end', rcube_utils::INPUT_GET)); + } + + echo $this->encode($events); + exit; + } + + + /**** Event invitation plugin hooks ****/ + + /** + * Handler for calendar/itip-status requests + */ + function event_itip_status() + { + $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); + + // find local copy of the referenced event + $driver = null; + + if(isset($data["uid"])) + $driver = $this->get_driver_by_event($data["uid"]); + + if($driver == null) + $driver = $this->get_default_driver(); + + $existing = $driver->get_event($data, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL); + + $itip = $this->load_itip(); + $response = $itip->get_itip_status($data, $existing); + + // get a list of writeable calendars to save new events to + if (!$existing && !$data['nosave'] && $response['action'] == 'rsvp' || $response['action'] == 'import') { + $calendars = $driver->list_calendars(calendar_driver::FILTER_PERSONAL); + $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'itip-saveto', 'is_escaped' => true)); + $calendar_select->add('--', ''); + $numcals = 0; + foreach ($calendars as $calendar) { + if ($calendar['editable']) { + $calendar_select->add($calendar['name'], $calendar['id']); + $numcals++; + } + } + if ($numcals <= 1) + $calendar_select = null; + } + + if ($calendar_select) { + $default_calendar = $this->get_default_calendar($data['sensitivity']); + $response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . ' ' . + $calendar_select->show($default_calendar['id'])); + } + else if ($data['nosave']) { + $response['select'] = html::tag('input', array('type' => 'hidden', 'name' => 'calendar', 'id' => 'itip-saveto', 'value' => '')); + } + + // render small agenda view for the respective day + if ($data['method'] == 'REQUEST' && !empty($data['date']) && $response['action'] == 'rsvp') { + $event_start = rcube_utils::anytodatetime($data['date']); + $day_start = new Datetime(gmdate('Y-m-d 00:00', $data['date']), $this->lib->timezone); + $day_end = new Datetime(gmdate('Y-m-d 23:59', $data['date']), $this->lib->timezone); + + // get events on that day from the user's personal calendars + $calendars = $driver->list_calendars(calendar_driver::FILTER_PERSONAL); + $events = $driver->load_events($day_start->format('U'), $day_end->format('U'), null, array_keys($calendars)); + usort($events, function($a, $b) { return $a['start'] > $b['start'] ? 1 : -1; }); + + $before = $after = array(); + foreach ($events as $event) { + // TODO: skip events with free_busy == 'free' ? + if ($event['uid'] == $data['uid'] || $event['end'] < $day_start || $event['start'] > $day_end) + continue; + else if ($event['start'] < $event_start) + $before[] = $this->mail_agenda_event_row($event); + else + $after[] = $this->mail_agenda_event_row($event); + } + + $response['append'] = array( + 'selector' => '.calendar-agenda-preview', + 'replacements' => array( + '%before%' => !empty($before) ? join("\n", array_slice($before, -3)) : html::div('event-row no-event', $this->gettext('noearlierevents')), + '%after%' => !empty($after) ? join("\n", array_slice($after, 0, 3)) : html::div('event-row no-event', $this->gettext('nolaterevents')), + ), + ); + } + + $this->rc->output->command('plugin.update_itip_object_status', $response); + } + + /** + * Handler for calendar/itip-remove requests + */ + function event_itip_remove() + { + $success = false; + $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST); + $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST); + $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST); + + // search for event if only UID is given + $driver = $this->get_driver_by_event($uid); + if ($event = $driver->get_event(array('uid' => $uid, '_instance' => $instance), calendar_driver::FILTER_WRITEABLE)) { + $event['_savemode'] = $savemode; + $success = $driver->remove_event($event, true); + } + + if ($success) { + $this->rc->output->show_message('calendar.successremoval', 'confirmation'); + } + else { + $this->rc->output->show_message('calendar.errorsaving', 'error'); + } + } + + /** + * Handler for URLs that allow an invitee to respond on his invitation mail + */ + public function itip_attend_response($p) + { + if ($p['action'] == 'attend') { + $this->ui->init(); + + $this->rc->output->set_env('task', 'calendar'); // override some env vars + $this->rc->output->set_env('refresh_interval', 0); + $this->rc->output->set_pagetitle($this->gettext('calendar')); + + $itip = $this->load_itip(); + $token = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC); + + // read event info stored under the given token + if ($invitation = $itip->get_invitation($token)) { + $this->token = $token; + $this->event = $invitation['event']; + + // show message about cancellation + if ($invitation['cancelled']) { + $this->invitestatus = html::div('rsvp-status declined', $itip->gettext('eventcancelled')); + } + // save submitted RSVP status + else if (!empty($_POST['rsvp'])) { + $status = null; + foreach (array('accepted','tentative','declined') as $method) { + if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) { + $status = $method; + break; + } + } + + // send itip reply to organizer + $invitation['event']['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); + if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) { + $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->gettext('youhave'.strtolower($status))); + } + else + $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1); + + // if user is logged in... + if ($this->rc->user->ID) { + $invitation = $itip->get_invitation($token); + $driver = $this->get_driver_by_cal($invitation['event']['calendar']); + + // save the event to his/her default calendar if not yet present + if (!$driver->get_event($this->event) && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) { + $invitation['event']['calendar'] = $calendar['id']; + if ($driver->new_event($invitation['event'])) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); + } + } + } + + $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform')); + $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox')); + + if (!$this->invitestatus) { + $this->itip->set_rsvp_actions(array('accepted','tentative','declined')); + $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons')); + } + + $this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']); + } + else + $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1); + + $this->rc->output->send('calendar.itipattend'); + } + } + + /** + * + */ + public function itip_event_inviteform($attrib) + { + $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token)); + return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show(); + } + + /** + * + */ + private function mail_agenda_event_row($event, $class = '') + { + $time = $event['allday'] ? $this->gettext('all-day') : + $this->rc->format_date($event['start'], $this->rc->config->get('time_format')) . ' - ' . + $this->rc->format_date($event['end'], $this->rc->config->get('time_format')); + + return html::div(rtrim('event-row ' . $class), + html::span('event-date', $time) . + html::span('event-title', Q($event['title'])) + ); + } + + /** + * + */ + public function mail_messages_list($p) + { + if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) { + foreach ($p['messages'] as $header) { + $part = new StdClass; + $part->mimetype = $header->ctype; + if (libcalendaring::part_is_vcalendar($part)) { + $header->list_flags['attachmentClass'] = 'ical'; + } + else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) { + // TODO: fetch bodystructure and search for ical parts. Maybe too expensive? + + if (!empty($header->structure) && is_array($header->structure->parts)) { + foreach ($header->structure->parts as $part) { + if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) { + $header->list_flags['attachmentClass'] = 'ical'; + break; + } + } + } + } + } + } + } + + /** + * Add UI element to copy event invitations or updates to the calendar + */ + public function mail_messagebody_html($p) + { + // load iCalendar functions (if necessary) + if (!empty($this->lib->ical_parts)) { + $this->get_ical(); + $this->load_itip(); + } + + $html = ''; + $has_events = false; + $ical_objects = $this->lib->get_mail_ical_objects(); + + // show a box for every event in the file + foreach ($ical_objects as $idx => $event) { + if ($event['_type'] != 'event') // skip non-event objects (#2928) + continue; + + $has_events = true; + + // get prepared inline UI for this event object + if ($ical_objects->method) { + $append = ''; + + // prepare a small agenda preview to be filled with actual event data on async request + if ($ical_objects->method == 'REQUEST') { + $append = html::div('calendar-agenda-preview', + html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' . + html::span('date', $this->rc->format_date($event['start'], $this->rc->config->get('date_format'))) + ) . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%'); + } + + $html .= html::div('calendar-invitebox', + $this->itip->mail_itip_inline_ui( + $event, + $ical_objects->method, + $ical_objects->mime_id . ':' . $idx, + 'calendar', + rcube_utils::anytodatetime($ical_objects->message_date), + $this->rc->url(array('task' => 'calendar')) . '&view=agendaDay&date=' . $event['start']->format('U') + ) . $append + ); + } + + // limit listing + if ($idx >= 3) + break; + } + + // prepend event boxes to message body + if ($html) { + $this->ui->init(); + $p['content'] = $html . $p['content']; + $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm'); + } + + // add "Save to calendar" button into attachment menu + if ($has_events) { + $this->add_button(array( + 'id' => 'attachmentsavecal', + 'name' => 'attachmentsavecal', + 'type' => 'link', + 'wrapper' => 'li', + 'command' => 'attachment-save-calendar', + 'class' => 'icon calendarlink', + 'classact' => 'icon calendarlink active', + 'innerclass' => 'icon calendar', + 'label' => 'calendar.savetocalendar', + ), 'attachmentmenu'); + } + + return $p; + } + + + /** + * Handler for POST request to import an event attached to a mail message + */ + public function mail_import_itip() + { + $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']); + + $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); + $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); + $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); + $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST); + $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST)); + $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)); + $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0; + $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST); + $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST); + + $error_msg = $this->gettext('errorimportingevent'); + $success = false; + $delegate = null; + + if ($status == 'delegated') { + $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false); + $delegate = reset($delegates); + + if (empty($delegate) || empty($delegate['mailto'])) { + $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error'); + return; + } + } + + // successfully parsed events? + if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { + // forward iTip request to delegatee + if ($delegate) { + $rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST)); + + $itip = $this->load_itip(); + if ($itip->delegate_to($event, $delegate, $rsvpme ? true : false)) { + $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation'); + } + else { + $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); + } + + // the delegator is set to non-participant, thus save as non-blocking + $event['free_busy'] = 'free'; + } + + // find writeable calendar to store event + $cal_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null; + + $calendar = null; + $driver = null; + + if($cal_id) { + $driver = $this->get_driver_by_cal($cal_id); + $calendars = $driver->list_calendars(false, true); + $calendar = $calendars[$cal_id]; + } + + $dontsave = ($_REQUEST['_folder'] === '' && $event['_method'] == 'REQUEST'); + + // select default calendar except user explicitly selected 'none' + if (!$calendar && !$dontsave) + $calendar = $this->get_default_calendar(true, $event['sensitivity'] == 'confidential'); + + if(!$driver) { + $driver = $this->get_driver_by_cal($calendar["id"]); + } + + // select default calendar except user explicitly selected 'none' + if (!$calendar && !$dontsave) + $calendar = $this->get_default_calendar($event['sensitivity']); + + $metadata = array( + 'uid' => $event['uid'], + '_instance' => $event['_instance'], + 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0, + 'sequence' => intval($event['sequence']), + 'fallback' => strtoupper($status), + 'method' => $event['_method'], + 'task' => 'calendar', + ); + + // update my attendee status according to submitted method + if (!empty($status)) { + $organizer = null; + $emails = $this->get_user_emails(); + foreach ($event['attendees'] as $i => $attendee) { + if ($attendee['role'] == 'ORGANIZER') { + $organizer = $attendee; + } + else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + $event['attendees'][$i]['status'] = strtoupper($status); + if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) + $event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute + + $metadata['attendee'] = $attendee['email']; + $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; + $reply_sender = $attendee['email']; + $event_attendee = $attendee; + } + } + + // add attendee with this user's default identity if not listed + if (!$reply_sender) { + $sender_identity = $this->rc->user->list_emails(true); + $event['attendees'][] = array( + 'name' => $sender_identity['name'], + 'email' => $sender_identity['email'], + 'role' => 'OPT-PARTICIPANT', + 'status' => strtoupper($status), + ); + $metadata['attendee'] = $sender_identity['email']; + } + } + + // save to calendar + if ($calendar && $calendar['editable']) { + // check for existing event with the same UID + $existing = $driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL); + + if ($existing) { + // forward savemode for correct updates of recurring events + $existing['_savemode'] = $savemode ?: $event['_savemode']; + + // only update attendee status + if ($event['_method'] == 'REPLY') { + // try to identify the attendee using the email sender address + $existing_attendee = -1; + $existing_attendee_emails = array(); + foreach ($existing['attendees'] as $i => $attendee) { + $existing_attendee_emails[] = $attendee['email']; + if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { + $existing_attendee = $i; + } + } + $event_attendee = null; + $update_attendees = array(); + foreach ($event['attendees'] as $attendee) { + if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) { + $event_attendee = $attendee; + $update_attendees[] = $attendee; + $metadata['fallback'] = $attendee['status']; + $metadata['attendee'] = $attendee['email']; + $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT'; + if ($attendee['status'] != 'DELEGATED') { + break; + } + } + // also copy delegate attendee + else if (!empty($attendee['delegated-from']) && + (stripos($attendee['delegated-from'], $event['_sender']) !== false || + stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) { + $update_attendees[] = $attendee; + if (!in_array($attendee['email'], $existing_attendee_emails)) { + $existing['attendees'][] = $attendee; + } + } + } + + // if delegatee has declined, set delegator's RSVP=True + if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) { + foreach ($existing['attendees'] as $i => $attendee) { + if ($attendee['email'] == $event_attendee['delegated-from']) { + $existing['attendees'][$i]['rsvp'] = true; + break; + } + } + } + + // found matching attendee entry in both existing and new events + if ($existing_attendee >= 0 && $event_attendee) { + $existing['attendees'][$existing_attendee] = $event_attendee; + $success = $driver->update_attendees($existing, $update_attendees); + } + // update the entire attendees block + else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) { + $existing['attendees'][] = $event_attendee; + $success = $driver->update_attendees($existing, $update_attendees); + } + else { + $error_msg = $this->gettext('newerversionexists'); + } + } + // delete the event when declined (#1670) + else if ($status == 'declined' && $delete) { + $deleted = $driver->remove_event($existing, true); + $success = true; + } + // import the (newer) event + else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) { + $event['id'] = $existing['id']; + $event['calendar'] = $existing['calendar']; + + // preserve my participant status for regular updates + if (empty($status)) { + $emails = $this->get_user_emails(); + foreach ($event['attendees'] as $i => $attendee) { + if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { + foreach ($existing['attendees'] as $j => $_attendee) { + if ($attendee['email'] == $_attendee['email']) { + $event['attendees'][$i] = $existing['attendees'][$j]; + break; + } + } + } + } + } + + // set status=CANCELLED on CANCEL messages + if ($event['_method'] == 'CANCEL') + $event['status'] = 'CANCELLED'; + // show me as free when declined (#1670) + if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') + $event['free_busy'] = 'free'; + + $success = $driver->edit_event($event); + } + else if (!empty($status)) { + $existing['attendees'] = $event['attendees']; + if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') // show me as free when declined (#1670) + $existing['free_busy'] = 'free'; + $success = $driver->edit_event($existing); + } + else + $error_msg = $this->gettext('newerversionexists'); + } + else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) { + if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') { + $event['free_busy'] = 'free'; + } + + // if the RSVP reply only refers to a single instance: + // store unmodified master event with current instance as exception + if (!empty($instance) && !empty($savemode) && $savemode != 'all') { + $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event'); + if ($master['recurrence'] && !$master['_instance']) { + // compute recurring events until this instance's date + if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) { + $recurrence_date->setTime(23,59,59); + + foreach ($driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) { + if ($recurring['_instance'] == $instance) { + // copy attendees block with my partstat to exception + $recurring['attendees'] = $event['attendees']; + $master['recurrence']['EXCEPTIONS'][] = $recurring; + $event = $recurring; // set reference for iTip reply + break; + } + } + + $master['calendar'] = $event['calendar'] = $calendar['id']; + $success = $driver->new_event($master); + } + else { + $master = null; + } + } + else { + $master = null; + } + } + + // save to the selected/default calendar + if (!$master) { + $event['calendar'] = $calendar['id']; + $success = $driver->new_event($event); + } + } + else if ($status == 'declined') + $error_msg = null; + } + else if ($status == 'declined' || $dontsave) + $error_msg = null; + else + $error_msg = $this->gettext('nowritecalendarfound'); + } + + if ($success) { + $message = $event['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully')); + $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation'); + } + + if ($success || $dontsave) { + $metadata['calendar'] = $event['calendar']; + $metadata['nosave'] = $dontsave; + $metadata['rsvp'] = intval($metadata['rsvp']); + $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']); + $this->rc->output->command('plugin.itip_message_processed', $metadata); + $error_msg = null; + } + else if ($error_msg) { + $this->rc->output->command('display_message', $error_msg, 'error'); + } + + // send iTip reply + if ($event['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) { + $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); + $itip = $this->load_itip(); + $itip->set_sender_email($reply_sender); + if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation'); + else + $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); + } + + $this->rc->output->send(); + } + + + /** + * Handler for calendar/itip-remove requests + */ + function mail_itip_decline_reply() + { + $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); + $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); + $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); + + if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') { + $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST); + + foreach ($event['attendees'] as $_attendee) { + if ($_attendee['role'] != 'ORGANIZER') { + $attendee = $_attendee; + break; + } + } + + $itip = $this->load_itip(); + if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel')) + $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation'); + else + $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); + } + else { + $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); + } + } + + /** + * Handler for calendar/itip-delegate requests + */ + function mail_itip_delegate() + { + // forward request to mail_import_itip() with the right status + $_POST['_status'] = $_REQUEST['_status'] = 'delegated'; + $this->mail_import_itip(); + } + + /** + * Import the full payload from a mail message attachment + */ + public function mail_import_attachment() + { + $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); + $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); + $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST); + $charset = RCMAIL_CHARSET; + + // establish imap connection + $imap = $this->rc->get_storage(); + $imap->set_mailbox($mbox); + + if ($uid && $mime_id) { + $part = $imap->get_message_part($uid, $mime_id); + if ($part->ctype_parameters['charset']) + $charset = $part->ctype_parameters['charset']; +// $headers = $imap->get_message_headers($uid); + + if ($part) { + $events = $this->get_ical()->import($part, $charset); + } + } + + $success = $existing = 0; + if (!empty($events)) { + // find writeable calendar to store event + $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null; + $driver = null; + if($cal_id) $driver = $this->get_driver_by_cal($cal_id); + else $driver = $this->get_driver_by_gpc(); + $calendars = $driver->list_calendars(calendar_driver::FILTER_PERSONAL); + + foreach ($events as $event) { + // save to calendar + $calendar = $calendars[$cal_id] ?: $this->get_default_calendar($event['sensitivity']); + if ($calendar && $calendar['editable'] && $event['_type'] == 'event') { + $event['calendar'] = $calendar['id']; + + if (!$driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) { + $success += (bool)$driver->new_event($event); + } + else { + $existing++; + } + } + } + } + + if ($success) { + $this->rc->output->command('display_message', $this->gettext(array( + 'name' => 'importsuccess', + 'vars' => array('nr' => $success), + )), 'confirmation'); + } + else if ($existing) { + $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning'); + } + else { + $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error'); + } + } + + /** + * Read email message and return contents for a new event based on that message + */ + public function mail_message2event() + { + $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); + $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST); + $event = array(); + + // establish imap connection + $imap = $this->rc->get_storage(); + $imap->set_mailbox($mbox); + $message = new rcube_message($uid); + + if ($message->headers) { + $event['title'] = trim($message->subject); + $event['description'] = trim($message->first_text_part()); + + $driver = $this->get_default_driver(); + + // add a reference to the email message + if ($msgref = $driver->get_message_reference($message->headers, $mbox)) { + $event['links'] = array($msgref); + } + // copy mail attachments to event + else if ($message->attachments) { + $eventid = 'cal-'; + if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) { + $_SESSION[self::SESSION_KEY] = array(); + $_SESSION[self::SESSION_KEY]['id'] = $eventid; + $_SESSION[self::SESSION_KEY]['attachments'] = array(); + } + + foreach ((array)$message->attachments as $part) { + $attachment = array( + 'data' => $imap->get_message_part($uid, $part->mime_id, $part), + 'size' => $part->size, + 'name' => $part->filename, + 'mimetype' => $part->mimetype, + 'group' => $eventid, + ); + + $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment); + + if ($attachment['status'] && !$attachment['abort']) { + $id = $attachment['id']; + $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']); + + // store new attachment in session + unset($attachment['status'], $attachment['abort'], $attachment['data']); + $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment; + + $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new' + $event['attachments'][] = $attachment; + } + } + } + + $this->rc->output->command('plugin.mail2event_dialog', $event); + } + else { + $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error'); + } + + $this->rc->output->send(); + } + + /** + * Handler for the 'message_compose' plugin hook. This will check for + * a compose parameter 'calendar_event' and create an attachment with the + * referenced event in iCal format + */ + public function mail_message_compose($args) + { + // set the submitted event ID as attachment + if (!empty($args['param']['calendar_event'])) { + list($cal, $id) = explode(':', $args['param']['calendar_event'], 2); + $driver = $this->get_driver_by_cal($cal); + if ($event = $driver->get_event(array('id' => $id, 'calendar' => $cal))) { + $filename = asciiwords($event['title']); + if (empty($filename)) + $filename = 'event'; + + // save ics to a temp file and register as attachment + $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal'); + file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($driver, 'get_attachment_body'))); + + $args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar'); + $args['param']['subject'] = $event['title']; + } + } + + return $args; + } + + + /** + * Get a list of email addresses of the current user (from login and identities) + */ + public function get_user_emails() + { + return $this->lib->get_user_emails(); + } + + + /** + * Build an absolute URL with the given parameters + */ + public function get_url($param = array()) + { + $param += array('task' => 'calendar'); + return $this->rc->url($param, true, true); + } + + + public function ical_feed_hash($source) + { + return base64_encode($this->rc->user->get_username() . ':' . $source); + } + + /** + * Handler for user_delete plugin hook + */ + public function user_delete($args) + { + // delete itipinvitations entries related to this user + $db = $this->rc->get_dbh(); + $table_itipinvitations = $db->table_name('itipinvitations', true); + $db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID); + + foreach($this->get_drivers() as $driver) + if(!$driver->user_delete($args)) + return false; + + return true; + } + + /** + * Magic getter for public access to protected members + */ + public function __get($name) + { + switch ($name) { + case 'ical': + return $this->get_ical(); + + case 'itip': + return $this->load_itip(); + + case 'driver': + $driver = $this->get_driver_by_gpc(true); + if(!$driver) $driver = $this->get_default_driver(); + return $driver; + } + + return null; + } + +} diff --git a/calendar/calendar_base.js b/calendar/calendar_base.js new file mode 100644 index 0000000..41ae8e5 --- /dev/null +++ b/calendar/calendar_base.js @@ -0,0 +1,139 @@ +/** + * Base Javascript class for the Calendar plugin + * + * @author Lazlo Westerhof + * @author Thomas Bruederli + * + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2010, Lazlo Westerhof + * Copyright (C) 2013-2015, Kolab Systems AG + * + * 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 . + * + * @licend The above is the entire license notice + * for the JavaScript code in this page. + */ + +// Basic setup for Roundcube calendar client class +function rcube_calendar(settings) +{ + // extend base class + rcube_libcalendaring.call(this, settings); + + // member vars + this.ui; + this.ui_loaded = false; + this.selected_attachment = null; + + // private vars + var me = this; + + // create new event from current mail message + this.create_from_mail = function(uid) + { + if (uid || (uid = rcmail.get_single_uid())) { + // load calendar UI (scripts and edit dialog template) + if (!this.ui_loaded) { + $.when( + $.getScript(rcmail.assets_path('plugins/calendar/calendar_ui.js')), + $.getScript(rcmail.assets_path('plugins/calendar/lib/js/fullcalendar.js')), + $.get(rcmail.url('calendar/inlineui'), function(html){ $(document.body).append(html); }, 'html') + ).then(function() { + // disable attendees feature (autocompletion and stuff is not initialized) + for (var c in rcmail.env.calendars) + rcmail.env.calendars[c].attendees = rcmail.env.calendars[c].resources = false; + + me.ui_loaded = true; + me.ui = new rcube_calendar_ui(me.settings); + me.create_from_mail(uid); // start over + }); + return; + } + else { + // get message contents for event dialog + var lock = rcmail.set_busy(true, 'loading'); + rcmail.http_post('calendar/mailtoevent', { + '_mbox': rcmail.env.mailbox, + '_uid': uid + }, lock); + } + } + }; + + // callback function triggered from server with contents for the new event + this.mail2event_dialog = function(event) + { + if (event.title) { + this.ui.add_event(event); + if (rcmail.message_list) + rcmail.message_list.blur(); + } + }; + + // handler for attachment-save-calendar commands + this.save_to_calendar = function(p) + { + // TODO: show dialog to select the calendar for importing + if (this.selected_attachment && window.rcube_libcalendaring) { + rcmail.http_post('calendar/mailimportattach', { + _uid: rcmail.env.uid, + _mbox: rcmail.env.mailbox, + _part: this.selected_attachment, + // _calendar: $('#calendar-attachment-saveto').val(), + }, rcmail.set_busy(true, 'itip.savingdata')); + } + } +} + + +/* calendar plugin initialization (for non-calendar tasks) */ +window.rcmail && rcmail.addEventListener('init', function(evt) { + if (rcmail.task != 'calendar') { + var cal = new rcube_calendar($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings)); + + // register create-from-mail command to message_commands array + if (rcmail.env.task == 'mail') { + rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() }); + rcmail.register_command('attachment-save-calendar', function() { cal.save_to_calendar() }); + rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) }); + rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.ui && cal.ui.unlock_saving(); }); + + if (rcmail.env.action != 'show') { + rcmail.env.message_commands.push('calendar-create-from-mail'); + rcmail.add_element($('')); + } + else { + rcmail.enable_command('calendar-create-from-mail', true); + } + + rcmail.addEventListener('beforemenu-open', function(p) { + if (p.menu == 'attachmentmenu') { + cal.selected_attachment = p.id; + var mimetype = rcmail.env.attachments[p.id]; + rcmail.enable_command('attachment-save-calendar', mimetype == 'text/calendar' || mimetype == 'text/x-vcalendar' || mimetype == 'application/ics'); + } + }); + } + } + + rcmail.register_command('plugin.calendar', function() { rcmail.switch_task('calendar'); }, true); + + rcmail.addEventListener('plugin.ping_url', function(p){ + var action = p.action; + p.action = p.event = null; + new Image().src = rcmail.url(action, p); + }); +}); diff --git a/calendar/calendar_ui.js b/calendar/calendar_ui.js new file mode 100644 index 0000000..452ea38 --- /dev/null +++ b/calendar/calendar_ui.js @@ -0,0 +1,4273 @@ +/** + * Client UI Javascript for the Calendar plugin + * + * @author Lazlo Westerhof + * @author Thomas Bruederli + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (C) 2010, Lazlo Westerhof + * Copyright (C) 2014-2015, Kolab Systems AG + * + * 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 . + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ + +// Roundcube calendar UI client class +function rcube_calendar_ui(settings) +{ + // extend base class + rcube_calendar.call(this, settings); + + /*** member vars ***/ + this.is_loading = false; + this.selected_event = null; + this.selected_calendar = null; + this.search_request = null; + this.saving_lock; + this.calendars = {}; + this.quickview_sources = []; + + + /*** private vars ***/ + var DAY_MS = 86400000; + var HOUR_MS = 3600000; + var me = this; + var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0); + var client_timezone = new Date().getTimezoneOffset(); + var day_clicked = day_clicked_ts = 0; + var ignore_click = false; + var event_defaults = { free_busy:'busy', alarms:'' }; + var event_attendees = []; + var calendars_list; + var calenders_search_list; + var calenders_search_container; + var search_calendars = {}; + var attendees_list; + var resources_list; + var resources_treelist; + var resources_data = {}; + var resources_index = []; + var resource_owners = {}; + var resources_events_source = { url:null, editable:false }; + var freebusy_ui = { workinhoursonly:false, needsupdate:false }; + var freebusy_data = {}; + var current_view = null; + var count_sources = []; + var exec_deferred = bw.ie6 ? 5 : 1; + var sensitivitylabels = { 'public':rcmail.gettext('public','calendar'), 'private':rcmail.gettext('private','calendar'), 'confidential':rcmail.gettext('confidential','calendar') }; + var ui_loading = rcmail.set_busy(true, 'loading'); + + // general datepicker settings + var datepicker_settings = { + // translate from fullcalendar format to datepicker format + dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'), + firstDay : settings['first_day'], + dayNamesMin: settings['days_short'], + monthNames: settings['months'], + monthNamesShort: settings['months'], + changeMonth: false, + showOtherMonths: true, + selectOtherMonths: true + }; + + // global fullcalendar settings + var fullcalendar_defaults = { + aspectRatio: 1, + ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone + monthNames : settings.months, + monthNamesShort : settings.months_short, + dayNames : settings.days, + dayNamesShort : settings.days_short, + firstDay : settings.first_day, + firstHour : settings.first_hour, + slotMinutes : 60/settings.timeslots, + timeFormat: { + '': settings.time_format, + agenda: settings.time_format + '{ - ' + settings.time_format + '}', + list: settings.time_format + '{ - ' + settings.time_format + '}', + table: settings.time_format + '{ - ' + settings.time_format + '}' + }, + axisFormat : settings.time_format, + columnFormat: { + month: 'ddd', // Mon + week: 'ddd ' + settings.date_short, // Mon 9/7 + day: 'dddd ' + settings.date_short, // Monday 9/7 + table: settings.date_agenda + }, + titleFormat: { + month: 'MMMM yyyy', + week: settings.dates_long, + day: 'dddd ' + settings['date_long'], + table: settings.dates_long + }, + listPage: 7, // advance one week in agenda view + listRange: settings.agenda_range, + listSections: settings.agenda_sections, + tableCols: ['handle', 'date', 'time', 'title', 'location'], + defaultView: rcmail.env.view || settings.default_view, + allDayText: rcmail.gettext('all-day', 'calendar'), + buttonText: { + prev: ' ◄ ', + next: ' ► ', + today: settings['today'], + day: rcmail.gettext('day', 'calendar'), + week: rcmail.gettext('week', 'calendar'), + month: rcmail.gettext('month', 'calendar'), + table: rcmail.gettext('agenda', 'calendar') + }, + listTexts: { + until: rcmail.gettext('until', 'calendar'), + past: rcmail.gettext('pastevents', 'calendar'), + today: rcmail.gettext('today', 'calendar'), + tomorrow: rcmail.gettext('tomorrow', 'calendar'), + thisWeek: rcmail.gettext('thisweek', 'calendar'), + nextWeek: rcmail.gettext('nextweek', 'calendar'), + thisMonth: rcmail.gettext('thismonth', 'calendar'), + nextMonth: rcmail.gettext('nextmonth', 'calendar'), + future: rcmail.gettext('futureevents', 'calendar'), + week: rcmail.gettext('weekofyear', 'calendar') + }, + currentTimeIndicator: settings.time_indicator, + // event rendering + eventRender: function(event, element, view) { + if (view.name != 'list' && view.name != 'table') { + var prefix = event.sensitivity && event.sensitivity != 'public' ? String(sensitivitylabels[event.sensitivity]).toUpperCase()+': ' : ''; + element.attr('title', prefix + event.title); + } + if (view.name != 'month') { + if (event.location) { + element.find('div.fc-event-title').after('
@ ' + Q(event.location) + '
'); + } + if (event.sensitivity && event.sensitivity != 'public') + element.find('div.fc-event-time').append(''); + if (event.recurrence) + element.find('div.fc-event-time').append(''); + if (event.alarms || (event.valarms && event.valarms.length)) + element.find('div.fc-event-time').append(''); + } + if (event.status) { + element.addClass('cal-event-status-' + String(event.status).toLowerCase()); + } + + element.attr('aria-label', event.title + ', ' + me.event_date_text(event, true)); + }, + // render element indicating more (invisible) events + overflowRender: function(data, element) { + element.html(rcmail.gettext('andnmore', 'calendar').replace('$nr', data.count)) + .click(function(e){ me.fisheye_view(data.date); }); + }, + // callback when a specific event is clicked + eventClick: function(event, ev, view) { + if (!event.temp && String(event.className).indexOf('fc-type-freebusy') < 0) + event_show_dialog(event, ev); + } + }; + + /*** imports ***/ + var Q = this.quote_html; + var text2html = this.text2html; + var event_date_text = this.event_date_text; + var parse_datetime = this.parse_datetime; + var date2unixtime = this.date2unixtime; + var fromunixtime = this.fromunixtime; + var parseISO8601 = this.parseISO8601; + var date2servertime = this.date2ISO8601; + var render_message_links = this.render_message_links; + + + /*** private methods ***/ + + // same as str.split(delimiter) but it ignores delimiters within quoted strings + var explode_quoted_string = function(str, delimiter) + { + var result = [], + strlen = str.length, + q, p, i, char, last; + + for (q = p = i = 0; i < strlen; i++) { + char = str.charAt(i); + if (char == '"' && last != '\\') { + q = !q; + } + else if (!q && char == delimiter) { + result.push(str.substring(p, i)); + p = i + 1; + } + last = char; + } + + result.push(str.substr(p)); + return result; + }; + + // Change the first charcter to uppercase + var ucfirst = function(str) + { + return str.charAt(0).toUpperCase() + str.substr(1); + }; + + // clone the given date object and optionally adjust time + var clone_date = function(date, adjust) + { + var d = new Date(date.getTime()); + + // set time to 00:00 + if (adjust == 1) { + d.setHours(0); + d.setMinutes(0); + } + // set time to 23:59 + else if (adjust == 2) { + d.setHours(23); + d.setMinutes(59); + } + + return d; + }; + + // fix date if jumped over a DST change + var fix_date = function(date) + { + if (date.getHours() == 23) + date.setTime(date.getTime() + HOUR_MS); + else if (date.getHours() > 0) + date.setHours(0); + }; + + var date2timestring = function(date, dateonly) + { + return date2servertime(date).replace(/[^0-9]/g, '').substr(0, (dateonly ? 8 : 14)); + } + + var format_datetime = function(date, mode, voice) + { + return me.format_datetime(date, mode, voice); + } + + var render_link = function(url) + { + var islink = false, href = url; + if (url.match(/^[fhtpsmailo]+?:\/\//i)) { + islink = true; + } + else if (url.match(/^[a-z0-9.-:]+(\/|$)/i)) { + islink = true; + href = 'http://' + url; + } + return islink ? '
' + Q(url) + '' : Q(url); + } + + // determine whether the given date is on a weekend + var is_weekend = function(date) + { + return date.getDay() == 0 || date.getDay() == 6; + }; + + var is_workinghour = function(date) + { + if (settings['work_start'] > settings['work_end']) + return date.getHours() >= settings['work_start'] || date.getHours() < settings['work_end']; + else + return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end']; + }; + + // check if the event has 'real' attendees, excluding the current user + var has_attendees = function(event) + { + return (event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email)); + }; + + // check if the current user is an attendee of this event + var is_attendee = function(event, role, email) + { + var emails = email ? ';'+email.toLowerCase() : settings.identity.emails; + for (var i=0; event.attendees && i < event.attendees.length; i++) { + if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) + return event.attendees[i]; + } + return false; + }; + + // check if the current user is the organizer + var is_organizer = function(event, email) + { + return is_attendee(event, 'ORGANIZER', email) || !event.id; + }; + + /** + * Check permissions on the given calendar object + */ + var has_permission = function(cal, perm) + { + // multiple chars means "either of" + if (String(perm).length > 1) { + for (var i=0; i < perm.length; i++) { + if (has_permission(cal, perm[i])) + return true; + } + } + + if (cal.rights && String(cal.rights).indexOf(perm) >= 0) { + return true; + } + + return (perm == 'i' && cal.editable) || (perm == 'v' && cal.editable); + } + + var load_attachment = function(event, att) + { + var query = { _id: att.id, _event: event.recurrence_id || event.id, _cal:event.calendar, _frame: 1 }; + if (event.rev) + query._rev = event.rev; + + // open attachment in frame if it's of a supported mimetype + if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) { + if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) { + return; + } + } + + query._frame = null; + query._download = 1; + rcmail.goto_url('get-attachment', query, false); + }; + + // build event attachments list + var event_show_attachments = function(list, container, event, edit) + { + var i, id, len, img, content, li, elem, + ul = document.createElement('UL'); + ul.className = 'attachmentslist'; + + for (i=0, len=list.length; i') + .attr('title', rcmail.gettext('delete')) + .attr('aria-label', rcmail.gettext('delete') + ' ' + Q(elem.name)) + .addClass('delete') + .click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; }); + + if (!rcmail.env.deleteicon) + content.html(rcmail.gettext('delete')); + else { + img = document.createElement('IMG'); + img.src = rcmail.env.deleteicon; + img.alt = rcmail.gettext('delete'); + content.append(img); + } + + content.appendTo(li); + } + + // name/link + content = $('') + .html(Q(elem.name)) + .addClass('file') + .click({event: event, att: elem}, function(e) { + load_attachment(e.data.event, e.data.att); + return false; + }) + .appendTo(li); + + ul.appendChild(li); + } + + if (edit && rcmail.gui_objects.attachmentlist) { + ul.id = rcmail.gui_objects.attachmentlist.id; + rcmail.gui_objects.attachmentlist = ul; + } + + container.empty().append(ul); + }; + + var remove_attachment = function(elem, id) + { + $(elem.parentNode).hide(); + rcmail.env.deleted_attachments.push(id); + delete rcmail.env.attachments[id]; + }; + + // event details dialog (show only) + var event_show_dialog = function(event, ev, temp) + { + var $dialog = $("#eventshow"); + var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false, rights:'lrs' }; + + if (!temp) + me.selected_event = event; + + if ($dialog.is(':ui-dialog')) + $dialog.dialog('close'); + + // remove status-* classes + $dialog.removeClass(function(i, oldclass) { + var oldies = String(oldclass).split(' '); + return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 || cls.indexOf('sensitivity-') === 0 }).join(' '); + }); + + // convert start/end dates if not done yet by fullcalendar + if (typeof event.start == 'string') + event.start = parseISO8601(event.start); + if (typeof event.end == 'string') + event.end = parseISO8601(event.end); + + // allow other plugins to do actions when event form is opened + rcmail.triggerEvent('calendar-event-init', {o: event}); + + $dialog.find('div.event-section, div.event-line').hide(); + $('#event-title').html(Q(event.title)).show(); + + if (event.location) + $('#event-location').html('@ ' + text2html(event.location)).show(); + if (event.description) + $('#event-description').show().children('.event-text').html(text2html(event.description, 300, 6)); + if (event.vurl) + $('#event-url').show().children('.event-text').html(render_link(event.vurl)); + + // render from-to in a nice human-readable way + // -> now shown in dialog title + // $('#event-date').html(Q(me.event_date_text(event))).show(); + + if (event.recurrence && event.recurrence_text) + $('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text)); + + if (event.valarms && event.alarms_text) + $('#event-alarm').show().children('.event-text').html(Q(event.alarms_text)); + + if (calendar.name) + $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).attr('class', 'event-text cal-'+calendar.id).css('color', calendar.textColor || calendar.color || ''); + if (event.categories) + $('#event-category').show().children('.event-text').html(Q(event.categories)).attr('class', 'event-text cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, '')); + if (event.free_busy) + $('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar'))); + if (event.priority > 0) { + var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ]; + $('#event-priority').show().children('.event-text').html(Q(event.priority+' '+priolabels[event.priority])); + } + + if (event.status) { + var status_lc = String(event.status).toLowerCase(); + $('#event-status').show().children('.event-text').html(Q(rcmail.gettext('status-'+status_lc,'calendar'))); + $dialog.addClass('status-'+status_lc); + } + if (event.sensitivity && event.sensitivity != 'public') { + $('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity])); + $dialog.addClass('sensitivity-'+event.sensitivity); + } + if (event.created || event.changed) { + var created = parseISO8601(event.created), + changed = parseISO8601(event.changed) + $('#event-created-changed .event-created').html(Q(created ? format_datetime(created) : rcmail.gettext('unknown','calendar'))) + $('#event-created-changed .event-changed').html(Q(changed ? format_datetime(changed) : rcmail.gettext('unknown','calendar'))) + $('#event-created-changed').show() + } + + // create attachments list + if ($.isArray(event.attachments)) { + event_show_attachments(event.attachments, $('#event-attachments').children('.event-text'), event); + if (event.attachments.length > 0) { + $('#event-attachments').show(); + } + } + else if (calendar.attachments) { + // fetch attachments, some drivers doesn't set 'attachments' prop of the event? + } + + // build attachments list + $('#event-links').hide(); + if ($.isArray(event.links) && event.links.length) { + render_message_links(event.links || [], $('#event-links').children('.event-text'), false, 'calendar'); + $('#event-links').show(); + } + + // list event attendees + if (calendar.attendees && event.attendees) { + // sort resources to the end + event.attendees.sort(function(a,b) { + var j = a.cutype == 'RESOURCE' ? 1 : 0, + k = b.cutype == 'RESOURCE' ? 1 : 0; + return (j - k); + }); + + var data, mystatus = null, rsvp, line, morelink, html = '', overflow = ''; + for (var j=0; j < event.attendees.length; j++) { + data = event.attendees[j]; + if (data.email) { + if (data.role == 'ORGANIZER') + organizer = true; + else if (settings.identity.emails.indexOf(';'+data.email) >= 0) { + mystatus = data.status.toLowerCase(); + if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) + rsvp = mystatus; + } + } + + line = event_attendee_html(data); + + if (morelink) + overflow += line; + else + html += line; + + // stop listing attendees + if (j == 7 && event.attendees.length >= 7) { + morelink = $('').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1)); + } + } + + if (html && (event.attendees.length > 1 || !organizer)) { + $('#event-attendees').show() + .children('.event-text') + .html(html) + .find('a.mailtolink').click(event_attendee_click); + + // display all attendees in a popup when clicking the "more" link + if (morelink) { + $('#event-attendees .event-text').append(morelink); + morelink.click(function(e){ + rcmail.show_popup_dialog( + '
' + html + overflow + '
', + rcmail.gettext('tabattendees','calendar'), + null, + { width:450, modal:false }); + $('#all-event-attendees a.mailtolink').click(event_attendee_click); + return false; + }) + } + } + + if (mystatus && !rsvp) { + $('#event-partstat').show().children('.changersvp') + .removeClass('accepted tentative declined delegated needs-action') + .addClass(mystatus) + .children('.event-text') + .html(Q(rcmail.gettext('itip' + mystatus, 'libcalendaring'))); + } + + var show_rsvp = rsvp && !is_organizer(event) && event.status != 'CANCELLED' && has_permission(calendar, 'v'); + $('#event-rsvp')[(show_rsvp ? 'show' : 'hide')](); + $('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true); + + if (show_rsvp && event.comment) + $('#event-rsvp-comment').show().children('.event-text').html(Q(event.comment)); + + $('#event-rsvp a.reply-comment-toggle').show(); + $('#event-rsvp .itip-reply-comment textarea').hide().val(''); + + if (event.recurrence && event.id) { + var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all')); + $('#event-rsvp .rsvp-buttons').addClass('recurring'); + } + else { + $('#event-rsvp .rsvp-buttons').removeClass('recurring'); + } + } + + var buttons = []; + if (!temp && calendar.editable && event.editable !== false) { + buttons.push({ + text: rcmail.gettext('edit', 'calendar'), + click: function() { + event_edit_dialog('edit', event); + } + }); + } + if (!temp && has_permission(calendar, 'td') && event.editable !== false) { + buttons.push({ + text: rcmail.gettext('delete', 'calendar'), + 'class': 'delete', + click: function() { + me.delete_event(event); + $dialog.dialog('close'); + } + }); + } + + if (!buttons.length) { + buttons.push({ + text: rcmail.gettext('close', 'calendar'), + click: function(){ + $dialog.dialog('close'); + } + }); + } + + // open jquery UI dialog + $dialog.dialog({ + modal: false, + resizable: !bw.ie6, + closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons + title: me.event_date_text(event), + open: function() { + $dialog.attr('aria-hidden', 'false'); + setTimeout(function(){ + $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus(); + }, 5); + }, + close: function() { + $dialog.dialog('destroy').attr('aria-hidden', 'true').hide(); + rcmail.command('menu-close','eventoptionsmenu'); + $('.libcal-rsvp-replymode').hide(); + }, + dragStart: function() { + rcmail.command('menu-close','eventoptionsmenu'); + $('.libcal-rsvp-replymode').hide(); + }, + resizeStart: function() { + rcmail.command('menu-close','eventoptionsmenu'); + $('.libcal-rsvp-replymode').hide(); + }, + buttons: buttons, + minWidth: 320, + width: 420 + }).show(); + + // remember opener element (to be focused on close) + $dialog.data('opener', ev && rcube_event.is_keyboard(ev) ? ev.target : null); + + // set voice title on dialog widget + $dialog.dialog('widget').removeAttr('aria-labelledby') + .attr('aria-label', me.event_date_text(event, true) + ', ', event.title); + + // set dialog size according to content + me.dialog_resize($dialog.get(0), $dialog.height(), 420); + + // add link for "more options" drop-down + if (!temp && !event.temporary && event.calendar != '_resource') { + $('') + .attr('href', '#') + .html(rcmail.gettext('eventoptions','calendar')) + .addClass('dropdown-link') + .click(function(e) { + return rcmail.command('menu-open','eventoptionsmenu', this, e) + }) + .appendTo($dialog.parent().find('.ui-dialog-buttonset')); + } + + rcmail.enable_command('event-history', calendar.history) + }; + + // render HTML code for displaying an attendee record + var event_attendee_html = function(data) + { + var dispname = Q(data.name || data.email), tooltip = ''; + + if (data.email) { + tooltip = data.email + '; ' + data.status; + dispname = '' + dispname + ''; + } + + if (data['delegated-to']) + tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to']; + else if (data['delegated-from']) + tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from']; + + return '' + dispname + ' '; + }; + + // event handler for clicks on an attendee link + var event_attendee_click = function(e) + { + var cutype = $(this).attr('data-cutype'), + mailto = this.href.substr(7); + if (rcmail.env.calendar_resources && cutype == 'RESOURCE') { + event_resources_dialog(mailto); + } + else { + rcmail.command('compose', mailto, e ? e.target : null, e); + } + return false; + }; + + // bring up the event dialog (jquery-ui popup) + var event_edit_dialog = function(action, event) + { + // copy opener element from show dialog + var op_elem = $("#eventshow:ui-dialog").data('opener'); + + // close show dialog first + $("#eventshow:ui-dialog").data('opener', null).dialog('close'); + + var $dialog = $('
'); + var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:true, rights: action=='new' ? 'lrwitd' : 'lrs' }; + me.selected_event = $.extend($.extend({}, event_defaults), event); // clone event object (with defaults) + event = me.selected_event; // change reference to clone + freebusy_ui.needsupdate = false; + + // reset dialog first + $('#eventtabs').get(0).reset(); + $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', false); + $('#event-panel-recurrence, #event-panel-attachments').removeClass('disabled'); + + // allow other plugins to do actions when event form is opened + rcmail.triggerEvent('calendar-event-init', {o: event}); + + // event details + var title = $('#edit-title').val(event.title || ''); + var location = $('#edit-location').val(event.location || ''); + var description = $('#edit-description').text(event.description || ''); + var vurl = $('#edit-url').val(event.vurl || ''); + var categories = $('#edit-categories').val(event.categories); + var calendars = $('#edit-calendar').val(event.calendar); + var eventstatus = $('#edit-event-status').val(event.status); + var freebusy = $('#edit-free-busy').val(event.free_busy); + var priority = $('#edit-priority').val(event.priority); + var sensitivity = $('#edit-sensitivity').val(event.sensitivity); + + var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000); + var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration); + var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show(); + var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); + var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show(); + var allday = $('#edit-allday').get(0); + var notify = $('#edit-attendees-donotify').get(0); + var invite = $('#edit-attendees-invite').get(0); + var comment = $('#edit-attendees-comment'); + + invite.checked = settings.itip_notify & 1 > 0; + notify.checked = has_attendees(event) && invite.checked; + + if (event.allDay) { + starttime.val("12:00").hide(); + endtime.val("13:00").hide(); + allday.checked = true; + } + else { + allday.checked = false; + } + + // set calendar selection according to permissions + calendars.find('option').each(function(i, opt) { + var cal = me.calendars[opt.value] || {}; + $(opt).prop('disabled', !(cal.editable || (action == 'new' && has_permission(cal, 'i')))) + }); + + // set alarm(s) + me.set_alarms_edit('#edit-alarms', action != 'new' && event.valarms && calendar.alarms ? event.valarms : []); + + // enable/disable alarm property according to backend support + $('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')](); + + // check categories drop-down: add value if not exists + if (event.categories && !categories.find("option[value='"+event.categories+"']").length) { + $('