aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Lange <DLange@git.local>2016-03-07 15:53:16 +0100
committerDaniel Lange <DLange@git.local>2016-03-07 15:53:16 +0100
commit50569114acdc64e7c7cae1498635d3f821517c30 (patch)
tree13d6fe76af33134fbfb2286930fb6603047f9299
parentc210d30de6c62e7f7867bb32651349ddf455d9e6 (diff)
downloadroundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.zip
roundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.tar.gz
roundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.tar.bz2
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
-rw-r--r--README.md103
-rw-r--r--calendar/.gitignore8
-rw-r--r--calendar/README_kolab78
-rw-r--r--calendar/TODO48
-rw-r--r--calendar/UPGRADING17
-rw-r--r--calendar/calendar.php3581
-rw-r--r--calendar/calendar_base.js139
-rw-r--r--calendar/calendar_ui.js4273
-rw-r--r--calendar/composer.json31
-rw-r--r--calendar/config.inc.php.dist198
-rw-r--r--calendar/drivers/caldav/SQL/mysql.initial.sql92
-rw-r--r--calendar/drivers/caldav/SQL/mysql/.keep_dir0
-rw-r--r--calendar/drivers/caldav/SQL/mysql/2014081300.sql24
-rw-r--r--calendar/drivers/caldav/SQL/mysql/2015022500.sql125
-rw-r--r--calendar/drivers/caldav/SQL/mysql/2015022700.sql14
-rw-r--r--calendar/drivers/caldav/SQL/postgres.initial.sql51
-rw-r--r--calendar/drivers/caldav/caldav_driver.php2036
-rw-r--r--calendar/drivers/caldav/caldav_sync.php253
-rw-r--r--calendar/drivers/calendar_driver.php819
-rw-r--r--calendar/drivers/database/SQL/mysql.initial.sql85
-rw-r--r--calendar/drivers/database/SQL/mysql/2012080600.sql3
-rw-r--r--calendar/drivers/database/SQL/mysql/2013011000.sql1
-rw-r--r--calendar/drivers/database/SQL/mysql/2013042700.sql1
-rw-r--r--calendar/drivers/database/SQL/mysql/2013051600.sql3
-rw-r--r--calendar/drivers/database/SQL/mysql/2014040900.sql3
-rw-r--r--calendar/drivers/database/SQL/mysql/2015022700.sql15
-rw-r--r--calendar/drivers/database/SQL/postgres.initial.sql109
-rw-r--r--calendar/drivers/database/SQL/postgres/2012080600.sql3
-rw-r--r--calendar/drivers/database/SQL/postgres/2013011000.sql1
-rw-r--r--calendar/drivers/database/SQL/postgres/2013042700.sql8
-rw-r--r--calendar/drivers/database/SQL/postgres/2013051600.sql3
-rw-r--r--calendar/drivers/database/SQL/postgres/2014040900.sql3
-rw-r--r--calendar/drivers/database/SQL/postgres/2015022700.sql9
-rw-r--r--calendar/drivers/database/SQL/sqlite.initial.sql79
-rw-r--r--calendar/drivers/database/SQL/sqlite/2013011000.sql1
-rw-r--r--calendar/drivers/database/SQL/sqlite/2013042700.sql1
-rw-r--r--calendar/drivers/database/SQL/sqlite/2013051600.sql63
-rw-r--r--calendar/drivers/database/SQL/sqlite/2014040900.sql67
-rw-r--r--calendar/drivers/database/SQL/sqlite/2015022700.sql79
-rw-r--r--calendar/drivers/database/database_driver.php1496
-rw-r--r--calendar/drivers/ical/SQL/mysql.initial.sql91
-rw-r--r--calendar/drivers/ical/SQL/mysql/.keep_dir0
-rw-r--r--calendar/drivers/ical/SQL/mysql/2015022500.sql124
-rw-r--r--calendar/drivers/ical/SQL/mysql/2015022700.sql14
-rw-r--r--calendar/drivers/ical/ical_driver.php1821
-rw-r--r--calendar/drivers/ical/ical_sync.php125
-rw-r--r--calendar/drivers/kolab/SQL/mysql.initial.sql32
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2012080600.sql11
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2013011000.sql1
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2014041700.sql1
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2014082600.sql2
-rw-r--r--calendar/drivers/kolab/SQL/oracle.initial.sql31
-rw-r--r--calendar/drivers/kolab/SQL/postgres.initial.sql32
-rw-r--r--calendar/drivers/kolab/kolab_calendar.php836
-rw-r--r--calendar/drivers/kolab/kolab_driver.php2526
-rw-r--r--calendar/drivers/kolab/kolab_invitation_calendar.php377
-rw-r--r--calendar/drivers/kolab/kolab_user_calendar.php432
-rw-r--r--calendar/drivers/ldap/resources_driver_ldap.php150
-rw-r--r--calendar/drivers/resources_driver.php114
l---------calendar/helpdocs/en_US/_static/_skin1
l---------calendar/helpdocs/en_US/_static/kolab/alarms-popup.png1
-rw-r--r--calendar/helpdocs/en_US/_static/kolab/calendar-acl.pngbin0 -> 108292 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/kolab/calendar-header.pngbin0 -> 13036 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/kolab/event-participants.pngbin0 -> 101779 bytes
l---------calendar/helpdocs/en_US/_static/kolab/event-resize.png1
l---------calendar/helpdocs/en_US/_static/kolab/itip-invitation.png1
l---------calendar/helpdocs/en_US/_static/kolab/itip-reply.png1
-rw-r--r--calendar/helpdocs/en_US/_static/larry/alarms-popup.pngbin0 -> 19181 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/calendar-acl.pngbin0 -> 28590 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/calendar-header.pngbin0 -> 23126 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/event-participants.pngbin0 -> 39276 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/event-resize.pngbin0 -> 23728 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/itip-invitation.pngbin0 -> 117940 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/itip-reply.pngbin0 -> 39276 bytes
-rw-r--r--calendar/helpdocs/en_US/importexport.rst43
-rw-r--r--calendar/helpdocs/en_US/index.rst18
-rw-r--r--calendar/helpdocs/en_US/invitations.rst55
-rw-r--r--calendar/helpdocs/en_US/manage.rst169
-rw-r--r--calendar/helpdocs/en_US/overview.rst138
-rw-r--r--calendar/helpdocs/en_US/settings.rst75
-rw-r--r--calendar/helpdocs/en_US/sharing.rst49
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/importexport.po97
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/index.po29
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/invitations.po104
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/manage.po343
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/overview.po267
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/settings.po179
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/sharing.po100
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/po/importexport.pot86
-rw-r--r--calendar/helpdocs/po/index.pot26
-rw-r--r--calendar/helpdocs/po/invitations.pot74
-rw-r--r--calendar/helpdocs/po/manage.pot250
-rw-r--r--calendar/helpdocs/po/overview.pot195
-rw-r--r--calendar/helpdocs/po/settings.pot146
-rw-r--r--calendar/helpdocs/po/sharing.pot74
-rw-r--r--calendar/lib/SabreDAV/.htaccess2
-rw-r--r--calendar/lib/SabreDAV/ChangeLog1111
-rw-r--r--calendar/lib/SabreDAV/LICENSE27
-rw-r--r--calendar/lib/SabreDAV/README.md30
-rw-r--r--calendar/lib/SabreDAV/composer.json61
-rw-r--r--calendar/lib/SabreDAV/composer.lock80
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php155
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php233
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php47
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php691
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php243
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php376
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php279
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php298
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php392
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php142
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php48
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php173
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php38
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php44
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php192
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php324
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php218
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php182
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php1338
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php32
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php180
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php180
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php134
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php74
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php227
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php102
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php88
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php40
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php45
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php16
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php163
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php72
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php116
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php526
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php342
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php315
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php221
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php80
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php18
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php166
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php333
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php260
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php20
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php20
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php706
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php72
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php260
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php108
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php26
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php87
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php101
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php63
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php65
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php112
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php99
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php57
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php491
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.icobin0 -> 4286 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.pngbin0 -> 7232 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.pngbin0 -> 4388 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.pngbin0 -> 5695 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.pngbin0 -> 3474 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.pngbin0 -> 2837 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.pngbin0 -> 3474 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.pngbin0 -> 5480 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php578
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php110
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php64
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php37
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php33
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php41
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php73
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php45
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php71
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php32
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php31
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php140
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php91
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php82
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php159
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php146
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php214
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php85
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php46
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php71
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php51
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php193
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php183
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php167
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php81
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php649
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php83
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php55
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php159
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php39
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php48
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php246
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php31
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php78
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php99
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php105
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php25
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php104
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php127
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php157
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php59
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php78
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php2178
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php90
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php108
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php121
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php91
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php289
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php193
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php133
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php124
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php64
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php191
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php155
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php83
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php74
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php42
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php1402
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php281
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php18
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php153
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php428
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php33
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php211
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php34
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php124
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php161
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php94
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php227
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php67
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php240
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php284
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php175
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php82
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/autoload.php25
-rw-r--r--calendar/lib/SabreDAV/vendor/autoload.php7
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/ClassLoader.php387
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php9
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php15
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php9
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_real.php50
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/installed.json52
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog70
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE27
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md384
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json31
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php405
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php108
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php244
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php107
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php70
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php68
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php46
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php68
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php181
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php109
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php172
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php322
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php187
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php100
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php12
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php442
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php125
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php245
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php180
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php223
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php1112
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php111
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php39
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php76
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php61
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php482
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php24
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php41
-rw-r--r--calendar/lib/caldav-client.php379
-rw-r--r--calendar/lib/calendar_itip.php240
-rw-r--r--calendar/lib/calendar_recurrence.php88
-rw-r--r--calendar/lib/calendar_ui.php918
-rw-r--r--calendar/lib/encryption.php166
-rw-r--r--calendar/lib/js/fullcalendar.js6845
-rw-r--r--calendar/localization/bg_BG.inc122
-rw-r--r--calendar/localization/ca_ES.inc266
-rw-r--r--calendar/localization/cs_CZ.inc273
-rw-r--r--calendar/localization/da_DK.inc274
-rw-r--r--calendar/localization/de_CH.inc184
-rw-r--r--calendar/localization/de_DE.inc284
-rw-r--r--calendar/localization/en_US.inc315
-rw-r--r--calendar/localization/es_AR.inc269
-rw-r--r--calendar/localization/es_ES.inc26
-rw-r--r--calendar/localization/fi_FI.inc274
-rw-r--r--calendar/localization/fr_FR.inc271
-rw-r--r--calendar/localization/hu_HU.inc216
-rw-r--r--calendar/localization/it_IT.inc273
-rw-r--r--calendar/localization/ja_JP.inc177
-rw-r--r--calendar/localization/nl_NL.inc229
-rw-r--r--calendar/localization/pl_PL.inc272
-rw-r--r--calendar/localization/pt_BR.inc213
-rw-r--r--calendar/localization/pt_PT.inc274
-rw-r--r--calendar/localization/ru_RU.inc277
-rw-r--r--calendar/localization/sk_SK.inc82
-rw-r--r--calendar/localization/sl_SI.inc273
-rw-r--r--calendar/localization/sv_SE.inc273
-rw-r--r--calendar/localization/th_TH.inc257
-rw-r--r--calendar/localization/uk_UA.inc228
-rw-r--r--calendar/localization/zh_CN.inc76
-rw-r--r--calendar/print.js176
-rw-r--r--calendar/skins/larry/README11
-rw-r--r--calendar/skins/larry/calendar.css2327
-rw-r--r--calendar/skins/larry/fullcalendar.css711
-rw-r--r--calendar/skins/larry/iehacks.css77
-rw-r--r--calendar/skins/larry/images/attendee-status.pngbin0 -> 2202 bytes
-rw-r--r--calendar/skins/larry/images/autocomplete.pngbin0 -> 558 bytes
-rw-r--r--calendar/skins/larry/images/badge_cancelled.pngbin0 -> 924 bytes
-rw-r--r--calendar/skins/larry/images/badge_confidential.pngbin0 -> 1522 bytes
-rw-r--r--calendar/skins/larry/images/badge_private.pngbin0 -> 1346 bytes
-rw-r--r--calendar/skins/larry/images/calendar.pngbin0 -> 613 bytes
-rw-r--r--calendar/skins/larry/images/calendars.pngbin0 -> 2582 bytes
-rw-r--r--calendar/skins/larry/images/eventicons.pngbin0 -> 217 bytes
-rw-r--r--calendar/skins/larry/images/focusview.pngbin0 -> 4224 bytes
-rw-r--r--calendar/skins/larry/images/freebusy-colors.pngbin0 -> 302 bytes
-rw-r--r--calendar/skins/larry/images/ical-attachment.pngbin0 -> 492 bytes
-rw-r--r--calendar/skins/larry/images/invitation.pngbin0 -> 1485 bytes
-rw-r--r--calendar/skins/larry/images/loading_blue.gifbin0 -> 847 bytes
-rw-r--r--calendar/skins/larry/images/sendinvitation.pngbin0 -> 337 bytes
-rw-r--r--calendar/skins/larry/images/toggle.gifbin0 -> 110 bytes
-rw-r--r--calendar/skins/larry/images/toolbar.pngbin0 -> 3662 bytes
-rw-r--r--calendar/skins/larry/print.css229
-rw-r--r--calendar/skins/larry/print.iehacks.css25
-rw-r--r--calendar/skins/larry/templates/attachment.html64
-rw-r--r--calendar/skins/larry/templates/calendar.html537
-rw-r--r--calendar/skins/larry/templates/eventedit.html133
-rw-r--r--calendar/skins/larry/templates/freebusylegend.html7
-rw-r--r--calendar/skins/larry/templates/itipattend.html37
-rw-r--r--calendar/skins/larry/templates/kolabacl.html26
-rw-r--r--calendar/skins/larry/templates/kolabform.html9
-rw-r--r--calendar/skins/larry/templates/print.html29
-rw-r--r--libcalendaring/README15
-rw-r--r--libcalendaring/composer.json25
-rw-r--r--libcalendaring/lib/.htaccess2
-rw-r--r--libcalendaring/lib/Horde_Date.php1304
-rw-r--r--libcalendaring/lib/Horde_Date_Recurrence.php1705
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component.php405
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VAlarm.php108
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VCalendar.php244
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VCard.php107
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VEvent.php70
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VFreeBusy.php68
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VJournal.php46
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Component/VTodo.php68
-rw-r--r--libcalendaring/lib/OldSabre/VObject/DateTimeParser.php181
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Document.php109
-rw-r--r--libcalendaring/lib/OldSabre/VObject/ElementList.php172
-rw-r--r--libcalendaring/lib/OldSabre/VObject/FreeBusyGenerator.php322
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Node.php187
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Parameter.php91
-rw-r--r--libcalendaring/lib/OldSabre/VObject/ParseException.php12
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property.php453
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property/Compound.php125
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property/DateTime.php245
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Property/MultiDateTime.php180
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Reader.php223
-rw-r--r--libcalendaring/lib/OldSabre/VObject/RecurrenceIterator.php1144
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Splitter/ICalendar.php111
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Splitter/SplitterInterface.php39
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Splitter/VCard.php76
-rw-r--r--libcalendaring/lib/OldSabre/VObject/StringUtil.php61
-rw-r--r--libcalendaring/lib/OldSabre/VObject/TimeZoneUtil.php527
-rw-r--r--libcalendaring/lib/OldSabre/VObject/Version.php24
-rw-r--r--libcalendaring/lib/OldSabre/VObject/includes.php41
-rw-r--r--libcalendaring/lib/libcalendaring_itip.php817
-rw-r--r--libcalendaring/lib/libcalendaring_recurrence.php155
-rw-r--r--libcalendaring/libcalendaring.js1192
-rw-r--r--libcalendaring/libcalendaring.php1637
-rw-r--r--libcalendaring/libvcalendar.php1362
-rw-r--r--libcalendaring/localization/bg_BG.inc25
-rw-r--r--libcalendaring/localization/ca_ES.inc15
-rw-r--r--libcalendaring/localization/cs_CZ.inc131
-rw-r--r--libcalendaring/localization/da_DK.inc85
-rw-r--r--libcalendaring/localization/de_CH.inc81
-rw-r--r--libcalendaring/localization/de_DE.inc135
-rw-r--r--libcalendaring/localization/en_US.inc165
-rw-r--r--libcalendaring/localization/es_AR.inc125
-rw-r--r--libcalendaring/localization/es_ES.inc59
-rw-r--r--libcalendaring/localization/et_EE.inc60
-rw-r--r--libcalendaring/localization/fi_FI.inc133
-rw-r--r--libcalendaring/localization/fr_FR.inc131
-rw-r--r--libcalendaring/localization/hu_HU.inc96
-rw-r--r--libcalendaring/localization/it_IT.inc85
-rw-r--r--libcalendaring/localization/ja_JP.inc77
-rw-r--r--libcalendaring/localization/nl_NL.inc89
-rw-r--r--libcalendaring/localization/pl_PL.inc12
-rw-r--r--libcalendaring/localization/pt_BR.inc85
-rw-r--r--libcalendaring/localization/pt_PT.inc12
-rw-r--r--libcalendaring/localization/ru_RU.inc139
-rw-r--r--libcalendaring/localization/sk_SK.inc9
-rw-r--r--libcalendaring/localization/sl_SI.inc15
-rw-r--r--libcalendaring/localization/sv_SE.inc12
-rw-r--r--libcalendaring/localization/th_TH.inc80
-rw-r--r--libcalendaring/localization/uk_UA.inc14
-rw-r--r--libcalendaring/localization/zh_CN.inc10
-rw-r--r--libcalendaring/skins/larry/libcal.css166
665 files changed, 125761 insertions, 0 deletions
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'] = "<run pwgen -sy 32 to get some nice random data for here>";
+
+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 (<db-name>) will usually be roundcubemail or roundcube.
+Check with `mysql -e 'show databases;'`.
+
+ $ mysql <db-name> < /var/www/htdocs/roundcube/plugins/calendar/drivers/database/SQL/mysql.initial.sql
+ $ mysql <db-name> < /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/<yourchoice>/SQL/<yourdatabase>.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 /<path-to-roundcube>/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-<driver> --version=<version> \
+ --dir=../plugins/calendar/drivers/<driver>/SQL
+
+[*] Replace <driver> with "database" or "kolab" (without quotes)
+[*] Replace <version> 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 @@
+<?php
+
+/**
+ * Calendar plugin for Roundcube webmail
+ *
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class 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)) .
+ ' &mdash; ' . $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) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $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('') . '&nbsp;' . $add_category->show(),
+ );
+
+ $this->rc->output->add_script('function rcube_calendar_add_category(){
+ var name = $("#rcmfd_new_category").val();
+ if (name.length) {
+ var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
+ var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
+ var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
+ $("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").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'])) . '&nbsp;' . 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]) . '&nbsp;' . $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://<roundcubeurl>/?_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') . '&nbsp;' .
+ $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 <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2013-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @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($('<a>'));
+ }
+ 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 <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @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: '&nbsp;&#9668;&nbsp;',
+ next: '&nbsp;&#9658;&nbsp;',
+ 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('<div class="fc-event-location">@&nbsp;' + Q(event.location) + '</div>');
+ }
+ if (event.sensitivity && event.sensitivity != 'public')
+ element.find('div.fc-event-time').append('<i class="fc-icon-sensitive"></i>');
+ if (event.recurrence)
+ element.find('div.fc-event-time').append('<i class="fc-icon-recurring"></i>');
+ if (event.alarms || (event.valarms && event.valarms.length))
+ element.find('div.fc-event-time').append('<i class="fc-icon-alarms"></i>');
+ }
+ 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 ? '<a href="' + Q(href) + '" target="_blank">' + Q(url) + '</a>' : 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<len; i++) {
+ elem = list[i];
+ li = document.createElement('LI');
+ li.className = elem.classname;
+
+ if (edit) {
+ rcmail.env.attachments[elem.id] = elem;
+ // delete icon
+ content = $('<a href="#delete" />')
+ .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 = $('<a href="#load" />')
+ .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 = $('<a href="#more" class="morelink"></a>').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(
+ '<div id="all-event-attendees" class="event-attendees">' + html + overflow + '</div>',
+ 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') {
+ $('<a>')
+ .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 = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+ }
+
+ 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 '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+ };
+
+ // 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 = $('<div>');
+ 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) {
+ $('<option>').attr('value', event.categories).text(event.categories).appendTo(categories).prop('selected', true);
+ }
+
+ if ($.isArray(event.links) && event.links.length) {
+ render_message_links(event.links, $('#edit-event-links .event-text'), true, 'calendar');
+ $('#edit-event-links').show();
+ }
+ else {
+ $('#edit-event-links').hide();
+ }
+
+ // show warning if editing a recurring event
+ if (event.id && event.recurrence) {
+ var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
+ $('#edit-recurring-warning').show();
+ $('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true).change();
+ }
+ else
+ $('#edit-recurring-warning').hide();
+
+ // init attendees tab
+ var organizer = !event.attendees || is_organizer(event),
+ allow_invitations = organizer || (calendar.owner && calendar.owner == 'anonymous') || settings.invite_shared;
+ event_attendees = [];
+ attendees_list = $('#edit-attendees-table > tbody').html('');
+ resources_list = $('#edit-resources-table > tbody').html('');
+ $('#edit-attendees-notify')[(allow_invitations && has_attendees(event) && (settings.itip_notify & 2) ? 'show' : 'hide')]();
+ $('#edit-localchanges-warning')[(has_attendees(event) && !(allow_invitations || (calendar.owner && is_organizer(event, calendar.owner))) ? 'show' : 'hide')]();
+
+ var load_attendees_tab = function()
+ {
+ var j, data, reply_selected = 0;
+ if (event.attendees) {
+ for (j=0; j < event.attendees.length; j++) {
+ data = event.attendees[j];
+ // reset attendee status
+ if (event._savemode == 'new' && data.role != 'ORGANIZER') {
+ data.status = 'NEEDS-ACTION';
+ delete data.noreply;
+ }
+ add_attendee(data, !allow_invitations);
+ if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply)
+ reply_selected++;
+ }
+ }
+
+ // make sure comment box is visible if at least one attendee has reply enabled
+ // or global "send invitations" checkbox is checked
+ $('#eventedit .attendees-commentbox')[(reply_selected || invite.checked ? 'show' : 'hide')]();
+
+ // select the correct organizer identity
+ var identity_id = 0;
+ $.each(settings.identities, function(i,v){
+ if (organizer && typeof organizer == 'object' && v == organizer.email) {
+ identity_id = i;
+ return false;
+ }
+ });
+ $('#edit-identities-list').val(identity_id);
+ $('#edit-attendees-form')[(allow_invitations?'show':'hide')]();
+ $('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')]();
+ };
+
+ // attachments
+ var load_attachments_tab = function()
+ {
+ rcmail.enable_command('remove-attachment', calendar.editable && !event.recurrence_id);
+ rcmail.env.deleted_attachments = [];
+ // we're sharing some code for uploads handling with app.js
+ rcmail.env.attachments = [];
+ rcmail.env.compose_id = event.id; // for rcmail.async_upload_form()
+
+ if ($.isArray(event.attachments)) {
+ event_show_attachments(event.attachments, $('#edit-attachments'), event, true);
+ }
+ else {
+ $('#edit-attachments > ul').empty();
+ // fetch attachments, some drivers doesn't set 'attachments' array for event?
+ }
+ };
+
+ // init dialog buttons
+ var buttons = [];
+
+ // save action
+ buttons.push({
+ text: rcmail.gettext('save', 'calendar'),
+ 'class': 'mainaction',
+ click: function() {
+ var start = parse_datetime(allday.checked ? '12:00' : starttime.val(), startdate.val());
+ var end = parse_datetime(allday.checked ? '13:00' : endtime.val(), enddate.val());
+
+ // basic input validatetion
+ if (start.getTime() > end.getTime()) {
+ alert(rcmail.gettext('invalideventdates', 'calendar'));
+ return false;
+ }
+
+ // post data to server
+ var data = {
+ calendar: event.calendar,
+ start: date2servertime(start),
+ end: date2servertime(end),
+ allday: allday.checked?1:0,
+ title: title.val(),
+ description: description.val(),
+ location: location.val(),
+ categories: categories.val(),
+ vurl: vurl.val(),
+ free_busy: freebusy.val(),
+ priority: priority.val(),
+ sensitivity: sensitivity.val(),
+ status: eventstatus.val(),
+ recurrence: me.serialize_recurrence(endtime.val()),
+ valarms: me.serialize_alarms('#edit-alarms'),
+ attendees: event_attendees,
+ links: me.selected_event.links,
+ deleted_attachments: rcmail.env.deleted_attachments,
+ attachments: []
+ };
+
+ // uploaded attachments list
+ for (var i in rcmail.env.attachments)
+ if (i.match(/^rcmfile(.+)/))
+ data.attachments.push(RegExp.$1);
+
+ // read attendee roles
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (data.attendees[i])
+ data.attendees[i].role = $(elem).val();
+ });
+
+ if (organizer)
+ data._identity = $('#edit-identities-list option:selected').val();
+
+ // don't submit attendees if only myself is added as organizer
+ if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email)
+ data.attendees = [];
+
+ // per-attendee notification suppression
+ var need_invitation = false;
+ if (allow_invitations) {
+ $.each(data.attendees, function (i, v) {
+ if (v.role != 'ORGANIZER') {
+ if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked') || v.cutype == 'RESOURCE') {
+ need_invitation = true;
+ delete data.attendees[i]['noreply'];
+ }
+ else if (settings.itip_notify > 0) {
+ data.attendees[i].noreply = 1;
+ }
+ }
+ });
+ }
+
+ // tell server to send notifications
+ if ((data.attendees.length || (event.id && event.attendees.length)) && allow_invitations && (notify.checked || invite.checked || need_invitation)) {
+ data._notify = settings.itip_notify;
+ data._comment = comment.val();
+ }
+
+ data.calendar = calendars.val();
+
+ if (event.id) {
+ data.id = event.id;
+ if (event.recurrence)
+ data._savemode = $('input.edit-recurring-savemode:checked').val();
+ if (data.calendar && data.calendar != event.calendar)
+ data._fromcalendar = event.calendar;
+ }
+
+ update_event(action, data);
+ $dialog.dialog("close");
+ } // end click:
+ });
+
+ if (event.id) {
+ buttons.push({
+ text: rcmail.gettext('delete', 'calendar'),
+ 'class': 'delete',
+ click: function() {
+ me.delete_event(event);
+ $dialog.dialog('close');
+ }
+ });
+ }
+
+ buttons.push({
+ text: rcmail.gettext('cancel', 'calendar'),
+ click: function() {
+ $dialog.dialog("close");
+ }
+ });
+
+ // show/hide tabs according to calendar's feature support
+ $('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
+ $('#edit-tab-resources')[(rcmail.env.calendar_resources?'show':'hide')]();
+ $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
+
+ // activate the first tab
+ $('#eventtabs').tabs('option', 'active', 0);
+
+ // hack: set task to 'calendar' to make all dialog actions work correctly
+ var comm_path_before = rcmail.env.comm_path;
+ rcmail.env.comm_path = comm_path_before.replace(/_task=[a-z]+/, '_task=calendar');
+
+ var editform = $("#eventedit");
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
+ closeOnEscape: false,
+ title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
+ open: function() {
+ editform.attr('aria-hidden', 'false');
+ },
+ close: function() {
+ editform.hide().attr('aria-hidden', 'true').appendTo(document.body);
+ $dialog.dialog("destroy").remove();
+ rcmail.ksearch_blur();
+ freebusy_data = {};
+ rcmail.env.comm_path = comm_path_before; // restore comm_path
+ if (op_elem)
+ $(op_elem).focus();
+ },
+ buttons: buttons,
+ minWidth: 500,
+ width: 600
+ }).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
+
+ // set dialog size according to form content
+ me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 550);
+
+ title.select();
+
+ // init other tabs asynchronously
+ window.setTimeout(function(){ me.set_recurrence_edit(event); }, exec_deferred);
+ if (calendar.attendees)
+ window.setTimeout(load_attendees_tab, exec_deferred);
+ if (calendar.attachments)
+ window.setTimeout(load_attachments_tab, exec_deferred);
+ };
+
+ // show event changelog in a dialog
+ var event_history_dialog = function(event)
+ {
+ if (!event.id || !window.libkolab_audittrail)
+ return false
+
+ // render dialog
+ var $dialog = libkolab_audittrail.object_history_dialog({
+ module: 'calendar',
+ container: '#eventhistory',
+ title: rcmail.gettext('objectchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
+
+ // callback function for list actions
+ listfunc: function(action, rev) {
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:action, e:{ id:event.id, calendar:event.calendar, rev: rev } }, me.loading_lock);
+ },
+
+ // callback function for comparing two object revisions
+ comparefunc: function(rev1, rev2) {
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'diff', e:{ id:event.id, calendar:event.calendar, rev1: rev1, rev2: rev2 } }, me.loading_lock);
+ }
+ });
+
+ $dialog.data('event', event);
+
+ // fetch changelog data
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'changelog', e:{ id:event.id, calendar:event.calendar } }, me.loading_lock);
+ };
+
+ // callback from server with changelog data
+ var render_event_changelog = function(data)
+ {
+ var $dialog = $('#eventhistory'),
+ event = $dialog.data('event');
+
+ if (data === false || !data.length || !event) {
+ // display 'unavailable' message
+ $('<div class="notfound-message event-dialog-message warning">' + rcmail.gettext('objectchangelognotavailable','calendar') + '</div>')
+ .insertBefore($dialog.find('.changelog-table').hide());
+ return;
+ }
+
+ data.module = 'calendar';
+ libkolab_audittrail.render_changelog(data, event, me.calendars[event.calendar]);
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 600);
+ };
+
+ // callback from server with event diff data
+ var event_show_diff = function(data)
+ {
+ var event = me.selected_event,
+ $dialog = $("#eventdiff");
+
+ $dialog.find('div.event-section, div.event-line, h1.event-title-new').hide().data('set', false).find('.index').html('');
+ $dialog.find('div.event-section.clone, div.event-line.clone').remove();
+
+ // always show event title and date
+ $('.event-title', $dialog).text(event.title).removeClass('event-text-old').show();
+ $('.event-date', $dialog).text(me.event_date_text(event)).show();
+
+ // show each property change
+ $.each(data.changes, function(i,change) {
+ var prop = change.property, r2, html = false,
+ row = $('div.event-' + prop, $dialog).first();
+
+ // special case: title
+ if (prop == 'title') {
+ $('.event-title', $dialog).addClass('event-text-old').text(change['old'] || '--');
+ $('.event-title-new', $dialog).text(change['new'] || '--').show();
+ }
+
+ // no display container for this property
+ if (!row.length) {
+ return true;
+ }
+
+ // clone row if already exists
+ if (row.data('set')) {
+ r2 = row.clone().addClass('clone').insertAfter(row);
+ row = r2;
+ }
+
+ // format dates
+ if (['start','end','changed'].indexOf(prop) >= 0) {
+ if (change['old']) change.old_ = me.format_datetime(parseISO8601(change['old']));
+ if (change['new']) change.new_ = me.format_datetime(parseISO8601(change['new']));
+ }
+ // render description text
+ else if (prop == 'description') {
+ // TODO: show real text diff
+ if (!change.diff_ && change['old']) change.old_ = text2html(change['old']);
+ if (!change.diff_ && change['new']) change.new_ = text2html(change['new']);
+ html = true;
+ }
+ // format attendees struct
+ else if (prop == 'attendees') {
+ if (change['old']) change.old_ = event_attendee_html(change['old']);
+ if (change['new']) change.new_ = event_attendee_html($.extend({}, change['old'] || {}, change['new']));
+ html = true;
+ }
+ // localize priority values
+ else if (prop == 'priority') {
+ var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
+ if (change['old']) change.old_ = change['old'] + ' ' + (priolabels[change['old']] || '');
+ if (change['new']) change.new_ = change['new'] + ' ' + (priolabels[change['new']] || '');
+ }
+ // localize status
+ else if (prop == 'status') {
+ var status_lc = String(event.status).toLowerCase();
+ if (change['old']) change.old_ = rcmail.gettext(String(change['old']).toLowerCase(), 'calendar');
+ if (change['new']) change.new_ = rcmail.gettext(String(change['new']).toLowerCase(), 'calendar');
+ }
+
+ // format attachments struct
+ if (prop == 'attachments') {
+ if (change['old']) event_show_attachments([change['old']], row.children('.event-text-old'), event, false);
+ else row.children('.event-text-old').text('--');
+ if (change['new']) event_show_attachments([$.extend({}, change['old'] || {}, change['new'])], row.children('.event-text-new'), event, false);
+ else row.children('.event-text-new').text('--');
+ // remove click handler as we're currentyl not able to display the according attachment contents
+ $('.attachmentslist li a', row).unbind('click').removeAttr('href');
+ }
+ else if (change.diff_) {
+ row.children('.event-text-diff').html(change.diff_);
+ row.children('.event-text-old, .event-text-new').hide();
+ }
+ else {
+ if (!html) {
+ // escape HTML characters
+ change.old_ = Q(change.old_ || change['old'] || '--')
+ change.new_ = Q(change.new_ || change['new'] || '--')
+ }
+ row.children('.event-text-old').html(change.old_ || change['old'] || '--');
+ row.children('.event-text-new').html(change.new_ || change['new'] || '--');
+ }
+
+ // display index number
+ if (typeof change.index != 'undefined') {
+ row.find('.index').html('(' + change.index + ')');
+ }
+
+ row.show().data('set', true);
+
+ // hide event-date line
+ if (prop == 'start' || prop == 'end')
+ $('.event-date', $dialog).hide();
+ });
+
+ var buttons = {};
+ buttons[rcmail.gettext('close', 'calendar')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('objectdiff','calendar').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + event.title,
+ 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();
+ },
+ buttons: buttons,
+ minWidth: 320,
+ width: 450
+ }).show();
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 400);
+ };
+
+ // close the event history dialog
+ var close_history_dialog = function()
+ {
+ $('#eventhistory, #eventdiff').each(function(i, elem) {
+ var $dialog = $(elem);
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+ });
+ }
+
+ // exports
+ this.event_show_diff = event_show_diff;
+ this.event_show_dialog = event_show_dialog;
+ this.event_history_dialog = event_history_dialog;
+ this.render_event_changelog = render_event_changelog;
+ this.close_history_dialog = close_history_dialog;
+
+ // open a dialog to display detailed free-busy information and to find free slots
+ var event_freebusy_dialog = function()
+ {
+ var $dialog = $('#eventfreebusy'),
+ event = me.selected_event;
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (!event_attendees.length)
+ return false;
+
+ // set form elements
+ var allday = $('#edit-allday').get(0);
+ var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
+ freebusy_ui.startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
+ freebusy_ui.starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
+ freebusy_ui.enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
+ freebusy_ui.endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
+
+ if (allday.checked) {
+ freebusy_ui.starttime.val("12:00").hide();
+ freebusy_ui.endtime.val("13:00").hide();
+ event.allDay = true;
+ }
+
+ // read attendee roles from drop-downs
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (event_attendees[i])
+ event_attendees[i].role = $(elem).val();
+ });
+
+ // render time slots
+ var now = new Date(), fb_start = new Date(), fb_end = new Date();
+ fb_start.setTime(event.start);
+ fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0);
+ fb_end.setTime(fb_start.getTime() + DAY_MS);
+
+ freebusy_data = { required:{}, all:{} };
+ freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet
+ freebusy_ui.numdays = Math.max(allday.checked ? 14 : 1, Math.ceil(duration * 2 / 86400));
+ freebusy_ui.interval = allday.checked ? 1440 : 60;
+ freebusy_ui.start = fb_start;
+ freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
+ render_freebusy_grid(0);
+
+ // render list of attendees
+ freebusy_ui.attendees = {};
+ var domid, dispname, data, role_html, list_html = '';
+ for (var i=0; i < event_attendees.length; i++) {
+ data = event_attendees[i];
+ dispname = Q(data.name || data.email);
+ domid = String(data.email).replace(rcmail.identifier_expr, '');
+ role_html = '<a class="attendee-role-toggle" id="rcmlia' + domid + '" title="' + Q(rcmail.gettext('togglerole', 'calendar')) + '">&nbsp;</a>';
+ list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + role_html + dispname + '</div>';
+
+ // clone attendees data for local modifications
+ freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data);
+ }
+
+ // add total row
+ list_html += '<div class="attendee spacer">&nbsp;</div>';
+ list_html += '<div class="attendee total">' + rcmail.gettext('reqallattendees','calendar') + '</div>';
+
+ $('#schedule-attendees-list').html(list_html)
+ .unbind('click.roleicons')
+ .bind('click.roleicons', function(e){
+ // toggle attendee status upon click on icon
+ if (e.target.id && e.target.id.match(/rcmlia(.+)/)) {
+ var attendee, domid = RegExp.$1,
+ roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'NON-PARTICIPANT', 'CHAIR' ];
+ if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') {
+ var req = attendee.role != 'OPT-PARTICIPANT' && attendee.role != 'NON-PARTICIPANT';
+ var j = $.inArray(attendee.role, roles);
+ j = (j+1) % roles.length;
+ attendee.role = roles[j];
+ $(e.target).parent().attr('class', 'attendee '+String(attendee.role).toLowerCase());
+
+ // update total display if required-status changed
+ if (req != (roles[j] != 'OPT-PARTICIPANT' && roles[j] != 'NON-PARTICIPANT')) {
+ compute_freebusy_totals();
+ update_freebusy_display(attendee.email);
+ }
+ }
+ }
+
+ return false;
+ });
+
+ // enable/disable buttons
+ $('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime()));
+
+ // dialog buttons
+ var buttons = {};
+
+ buttons[rcmail.gettext('select', 'calendar')] = function() {
+ $('#edit-startdate').val(freebusy_ui.startdate.val());
+ $('#edit-starttime').val(freebusy_ui.starttime.val());
+ $('#edit-enddate').val(freebusy_ui.enddate.val());
+ $('#edit-endtime').val(freebusy_ui.endtime.val());
+
+ // write role changes back to main dialog
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (event_attendees[i] && freebusy_ui.attendees[i]) {
+ event_attendees[i].role = freebusy_ui.attendees[i].role;
+ $(elem).val(event_attendees[i].role);
+ }
+ });
+
+ if (freebusy_ui.needsupdate)
+ update_freebusy_status(me.selected_event);
+ freebusy_ui.needsupdate = false;
+ $dialog.dialog("close");
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ $dialog.dialog({
+ modal: true,
+ resizable: true,
+ closeOnEscape: (!bw.ie6 && !bw.ie7),
+ title: rcmail.gettext('scheduletime', 'calendar'),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false').find('#shedule-find-next, #shedule-find-prev').not(':disabled').first().focus();
+ },
+ close: function() {
+ if (bw.ie6)
+ $("#edit-attendees-table").css('visibility','visible');
+ $dialog.dialog("destroy").attr('aria-hidden', 'true').hide();
+ // TODO: focus opener button
+ },
+ resizeStop: function() {
+ render_freebusy_overlay();
+ },
+ buttons: buttons,
+ minWidth: 640,
+ width: 850
+ }).show();
+
+ // hide edit dialog on IE6 because of drop-down elements
+ if (bw.ie6)
+ $("#edit-attendees-table").css('visibility','hidden');
+
+ // adjust dialog size to fit grid without scrolling
+ var gridw = $('#schedule-freebusy-times').width();
+ var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1;
+ me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow));
+
+ // fetch data from server
+ freebusy_ui.loading = 0;
+ load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
+ };
+
+ // render an HTML table showing free-busy status for all the event attendees
+ var render_freebusy_grid = function(delta)
+ {
+ if (delta) {
+ freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
+ fix_date(freebusy_ui.start);
+
+ // skip weekends if in workinhoursonly-mode
+ if (Math.abs(delta) == 1 && freebusy_ui.workinhoursonly) {
+ while (is_weekend(freebusy_ui.start))
+ freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
+ fix_date(freebusy_ui.start);
+ }
+
+ freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
+ }
+
+ var dayslots = Math.floor(1440 / freebusy_ui.interval);
+ var date_format = 'ddd '+ (dayslots <= 2 ? settings.date_short : settings.date_format);
+ var lastdate, datestr, css,
+ curdate = new Date(),
+ allday = (freebusy_ui.interval == 1440),
+ times_css = (allday ? 'allday ' : ''),
+ dates_row = '<tr class="dates">',
+ times_row = '<tr class="times">',
+ slots_row = '';
+ for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) {
+ curdate.setTime(t);
+ datestr = fc.fullCalendar('formatDate', curdate, date_format);
+ if (datestr != lastdate) {
+ if (lastdate && !allday) break;
+ dates_row += '<th colspan="' + dayslots + '" class="boxtitle date' + $.fullCalendar.formatDate(curdate, 'ddMMyyyy') + '">' + Q(datestr) + '</th>';
+ lastdate = datestr;
+ }
+
+ // set css class according to working hours
+ css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours';
+ times_row += '<td class="' + times_css + css + '" id="t-' + Math.floor(t/1000) + '">' + Q(allday ? rcmail.gettext('all-day','calendar') : $.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
+ slots_row += '<td class="' + css + ' unknown">&nbsp;</td>';
+
+ t += freebusy_ui.interval * 60000;
+ }
+ dates_row += '</tr>';
+ times_row += '</tr>';
+
+ // render list of attendees
+ var domid, data, list_html = '', times_html = '';
+ for (var i=0; i < event_attendees.length; i++) {
+ data = event_attendees[i];
+ domid = String(data.email).replace(rcmail.identifier_expr, '');
+ times_html += '<tr id="fbrow' + domid + '">' + slots_row + '</tr>';
+ }
+
+ // add line for all/required attendees
+ times_html += '<tr class="spacer"><td colspan="' + (dayslots * freebusy_ui.numdays) + '">&nbsp;</td>';
+ times_html += '<tr id="fbrowall">' + slots_row + '</tr>';
+
+ var table = $('#schedule-freebusy-times');
+ table.children('thead').html(dates_row + times_row);
+ table.children('tbody').html(times_html);
+
+ // initialize event handlers on grid
+ if (!freebusy_ui.grid_events) {
+ freebusy_ui.grid_events = true;
+ table.children('thead').click(function(e){
+ // move event to the clicked date/time
+ if (e.target.id && e.target.id.match(/t-(\d+)/)) {
+ var newstart = new Date(RegExp.$1 * 1000);
+ // set time to 00:00
+ if (me.selected_event.allDay) {
+ newstart.setMinutes(0);
+ newstart.setHours(0);
+ }
+ update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
+ render_freebusy_overlay();
+ }
+ })
+ }
+
+ // if we have loaded free-busy data, show it
+ if (!freebusy_ui.loading) {
+ if (freebusy_ui.start < freebusy_data.start || freebusy_ui.end > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) {
+ load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
+ }
+ else {
+ for (var email, i=0; i < event_attendees.length; i++) {
+ if ((email = event_attendees[i].email))
+ update_freebusy_display(email);
+ }
+ }
+ }
+
+ // render current event date/time selection over grid table
+ // use timeout to let the dom attributes (width/height/offset) be set first
+ window.setTimeout(function(){ render_freebusy_overlay(); }, 10);
+ };
+
+ // render overlay element over the grid to visiualize the current event date/time
+ var render_freebusy_overlay = function()
+ {
+ var overlay = $('#schedule-event-time');
+ if (me.selected_event.end.getTime() <= freebusy_ui.start.getTime() || me.selected_event.start.getTime() >= freebusy_ui.end.getTime()) {
+ overlay.hide();
+ if (overlay.data('isdraggable'))
+ overlay.draggable('disable');
+ }
+ else {
+ var table = $('#schedule-freebusy-times'),
+ width = 0,
+ pos = { top:table.children('thead').height(), left:0 },
+ eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)),
+ eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60,
+ slotstart = date2unixtime(freebusy_ui.start),
+ slotsize = freebusy_ui.interval * 60,
+ slotend, fraction, $cell;
+
+ // iterate through slots to determine position and size of the overlay
+ table.children('thead').find('td').each(function(i, cell){
+ slotend = slotstart + slotsize - 1;
+ // event starts in this slot: compute left
+ if (eventstart >= slotstart && eventstart <= slotend) {
+ fraction = 1 - (slotend - eventstart) / slotsize;
+ pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction);
+ }
+ // event ends in this slot: compute width
+ if (eventend >= slotstart && eventend <= slotend) {
+ fraction = 1 - (slotend - eventend) / slotsize;
+ width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left;
+ }
+
+ slotstart = slotstart + slotsize;
+ });
+
+ if (!width)
+ width = table.width() - pos.left;
+
+ // overlay is visible
+ if (width > 0) {
+ overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show();
+
+ // configure draggable
+ if (!overlay.data('isdraggable')) {
+ overlay.draggable({
+ axis: 'x',
+ scroll: true,
+ stop: function(e, ui){
+ // convert pixels to time
+ var px = ui.position.left;
+ var range_p = $('#schedule-freebusy-times').width();
+ var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime();
+ var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p));
+ newstart.setSeconds(0); newstart.setMilliseconds(0);
+ // snap to day boundaries
+ if (me.selected_event.allDay) {
+ if (newstart.getHours() >= 12) // snap to next day
+ newstart.setTime(newstart.getTime() + DAY_MS);
+ newstart.setMinutes(0);
+ newstart.setHours(0);
+ }
+ else {
+ // round to 5 minutes
+ var round = newstart.getMinutes() % 5;
+ if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000);
+ else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000);
+ }
+ // update event times and display
+ update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
+ if (me.selected_event.allDay)
+ render_freebusy_overlay();
+ }
+ }).data('isdraggable', true);
+ }
+ else
+ overlay.draggable('enable');
+ }
+ else
+ overlay.draggable('disable').hide();
+ }
+
+ };
+
+
+ // fetch free-busy information for each attendee from server
+ var load_freebusy_data = function(from, interval)
+ {
+ var start = new Date(from.getTime() - DAY_MS * 2); // start 2 days before event
+ fix_date(start);
+ var end = new Date(start.getTime() + DAY_MS * Math.max(14, freebusy_ui.numdays + 7)); // load min. 14 days
+ freebusy_ui.numrequired = 0;
+ freebusy_data.all = [];
+ freebusy_data.required = [];
+
+ // load free-busy information for every attendee
+ var domid, email;
+ for (var i=0; i < event_attendees.length; i++) {
+ if ((email = event_attendees[i].email)) {
+ domid = String(email).replace(rcmail.identifier_expr, '');
+ $('#rcmli' + domid).addClass('loading');
+ freebusy_ui.loading++;
+
+ $.ajax({
+ type: 'GET',
+ dataType: 'json',
+ url: rcmail.url('freebusy-times'),
+ data: { email:email, start:date2servertime(clone_date(start, 1)), end:date2servertime(clone_date(end, 2)), interval:interval, _remote:1 },
+ success: function(data) {
+ freebusy_ui.loading--;
+
+ // find attendee
+ var attendee = null;
+ for (var i=0; i < event_attendees.length; i++) {
+ if (freebusy_ui.attendees[i].email == data.email) {
+ attendee = freebusy_ui.attendees[i];
+ break;
+ }
+ }
+
+ // copy data to member var
+ var ts, req = attendee.role != 'OPT-PARTICIPANT';
+ freebusy_data.start = parseISO8601(data.start);
+ freebusy_data[data.email] = {};
+ for (var i=0; i < data.slots.length; i++) {
+ ts = data.times[i] + '';
+ freebusy_data[data.email][ts] = data.slots[i];
+
+ // set totals
+ if (!freebusy_data.required[ts])
+ freebusy_data.required[ts] = [0,0,0,0];
+ if (req)
+ freebusy_data.required[ts][data.slots[i]]++;
+
+ if (!freebusy_data.all[ts])
+ freebusy_data.all[ts] = [0,0,0,0];
+ freebusy_data.all[ts][data.slots[i]]++;
+ }
+ freebusy_data.end = parseISO8601(data.end);
+ freebusy_data.interval = data.interval;
+
+ // hide loading indicator
+ var domid = String(data.email).replace(rcmail.identifier_expr, '');
+ $('#rcmli' + domid).removeClass('loading');
+
+ // update display
+ update_freebusy_display(data.email);
+ }
+ });
+
+ // count required attendees
+ if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT')
+ freebusy_ui.numrequired++;
+ }
+ }
+ };
+
+ // re-calculate total status after role change
+ var compute_freebusy_totals = function()
+ {
+ freebusy_ui.numrequired = 0;
+ freebusy_data.all = [];
+ freebusy_data.required = [];
+
+ var email, req, status;
+ for (var i=0; i < event_attendees.length; i++) {
+ if (!(email = event_attendees[i].email))
+ continue;
+
+ req = freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT';
+ if (req)
+ freebusy_ui.numrequired++;
+
+ for (var ts in freebusy_data[email]) {
+ if (!freebusy_data.required[ts])
+ freebusy_data.required[ts] = [0,0,0,0];
+ if (!freebusy_data.all[ts])
+ freebusy_data.all[ts] = [0,0,0,0];
+
+ status = freebusy_data[email][ts];
+ freebusy_data.all[ts][status]++;
+
+ if (req)
+ freebusy_data.required[ts][status]++;
+ }
+ }
+ };
+
+ // update free-busy grid with status loaded from server
+ var update_freebusy_display = function(email)
+ {
+ var status_classes = ['unknown','free','busy','tentative','out-of-office'];
+ var domid = String(email).replace(rcmail.identifier_expr, '');
+ var row = $('#fbrow' + domid);
+ var rowall = $('#fbrowall').children();
+ var dateonly = freebusy_ui.interval > 60,
+ t, ts = date2timestring(freebusy_ui.start, dateonly),
+ curdate = new Date(),
+ fbdata = freebusy_data[email];
+
+ if (fbdata && fbdata[ts] !== undefined && row.length) {
+ t = freebusy_ui.start.getTime();
+ row.children().each(function(i, cell){
+ curdate.setTime(t);
+ ts = date2timestring(curdate, dateonly);
+ cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown');
+
+ // also update total row if all data was loaded
+ if (freebusy_ui.loading == 0 && freebusy_data.all[ts] && (cell = rowall.get(i))) {
+ var workinghours = cell.className.indexOf('workinghours') >= 0;
+ var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown';
+ req_status = freebusy_data.required[ts][2] ? 'busy' : 'free';
+ for (var j=1; j < status_classes.length; j++) {
+ if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired)
+ req_status = status_classes[j];
+ if (freebusy_data.all[ts][j] == event_attendees.length)
+ all_status = status_classes[j];
+ }
+
+ cell.className = (workinghours ? 'workinghours ' : 'offhours ') + req_status + ' all-' + all_status;
+ }
+
+ t += freebusy_ui.interval * 60000;
+ });
+ }
+ };
+
+ // write changed event date/times back to form fields
+ var update_freebusy_dates = function(start, end)
+ {
+ // fix all-day evebt times
+ if (me.selected_event.allDay) {
+ var numdays = Math.floor((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / DAY_MS);
+ start.setHours(12);
+ start.setMinutes(0);
+ end.setTime(start.getTime() + numdays * DAY_MS);
+ end.setHours(13);
+ end.setMinutes(0);
+ }
+ me.selected_event.start = start;
+ me.selected_event.end = end;
+ freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format']));
+ freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format']));
+ freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format']));
+ freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format']));
+ freebusy_ui.needsupdate = true;
+ };
+
+ // attempt to find a time slot where all attemdees are available
+ var freebusy_find_slot = function(dir)
+ {
+ // exit if free-busy data isn't available yet
+ if (!freebusy_data || !freebusy_data.start)
+ return false;
+
+ var event = me.selected_event,
+ eventstart = clone_date(event.start, event.allDay ? 1 : 0).getTime(), // calculate with integers
+ eventend = clone_date(event.end, event.allDay ? 2 : 0).getTime(),
+ duration = eventend - eventstart - (event.allDay ? HOUR_MS : 0), /* make sure we don't cross day borders on DST change */
+ sinterval = freebusy_data.interval * 60000,
+ intvlslots = 1,
+ numslots = Math.ceil(duration / sinterval),
+ checkdate, slotend, email, ts, slot, slotdate = new Date();
+
+ // shift event times to next possible slot
+ eventstart += sinterval * intvlslots * dir;
+ eventend += sinterval * intvlslots * dir;
+
+ // iterate through free-busy slots and find candidates
+ var candidatecount = 0, candidatestart = candidateend = success = false;
+ for (slot = dir > 0 ? freebusy_data.start.getTime() : freebusy_data.end.getTime() - sinterval;
+ (dir > 0 && slot < freebusy_data.end.getTime()) || (dir < 0 && slot >= freebusy_data.start.getTime());
+ slot += sinterval * dir) {
+ slotdate.setTime(slot);
+ // fix slot if just crossed a DST change
+ if (event.allDay) {
+ fix_date(slotdate);
+ slot = slotdate.getTime();
+ }
+ slotend = slot + sinterval;
+
+ if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
+ continue;
+
+ // respect workingours setting
+ if (freebusy_ui.workinhoursonly) {
+ if (is_weekend(slotdate) || (freebusy_data.interval <= 60 && !is_workinghour(slotdate))) { // skip off-hours
+ candidatestart = candidateend = false;
+ candidatecount = 0;
+ continue;
+ }
+ }
+
+ if (!candidatestart)
+ candidatestart = slot;
+
+ // check freebusy data for all attendees
+ ts = date2timestring(slotdate, freebusy_data.interval > 60);
+ for (var i=0; i < event_attendees.length; i++) {
+ if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][ts] > 1) {
+ candidatestart = candidateend = false;
+ break;
+ }
+ }
+
+ // occupied slot
+ if (!candidatestart) {
+ slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir;
+ candidatecount = 0;
+ continue;
+ }
+
+ // set candidate end to slot end time
+ candidatecount++;
+ if (dir < 0 && !candidateend)
+ candidateend = slotend;
+
+ // if candidate is big enough, this is it!
+ if (candidatecount == numslots) {
+ if (dir > 0) {
+ event.start.setTime(candidatestart);
+ event.end.setTime(candidatestart + duration);
+ }
+ else {
+ event.end.setTime(candidateend);
+ event.start.setTime(candidateend - duration);
+ }
+ success = true;
+ break;
+ }
+ }
+
+ // update event date/time display
+ if (success) {
+ update_freebusy_dates(event.start, event.end);
+
+ // move freebusy grid if necessary
+ var offset = Math.ceil((event.start.getTime() - freebusy_ui.end.getTime()) / DAY_MS);
+ if (event.start.getTime() >= freebusy_ui.end.getTime())
+ render_freebusy_grid(Math.max(1, offset));
+ else if (event.end.getTime() <= freebusy_ui.start.getTime())
+ render_freebusy_grid(Math.min(-1, offset));
+ else
+ render_freebusy_overlay();
+
+ var now = new Date();
+ $('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
+
+ // speak new selection
+ rcmail.display_message(rcmail.gettext('suggestedslot', 'calendar') + ': ' + me.event_date_text(event, true), 'voice');
+ }
+ else {
+ alert(rcmail.gettext('noslotfound','calendar'));
+ }
+ };
+
+
+ // update event properties and attendees availability if event times have changed
+ var event_times_changed = function()
+ {
+ if (me.selected_event) {
+ var allday = $('#edit-allday').get(0);
+ me.selected_event.allDay = allday.checked;
+ me.selected_event.start = parse_datetime(allday.checked ? '12:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
+ me.selected_event.end = parse_datetime(allday.checked ? '13:00' : $('#edit-endtime').val(), $('#edit-enddate').val());
+ if (event_attendees)
+ freebusy_ui.needsupdate = true;
+ $('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000));
+ }
+ };
+
+
+ // add the given list of participants
+ var add_attendees = function(names, params)
+ {
+ names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
+
+ // parse name/email pairs
+ var item, email, name, success = false;
+ for (var i=0; i < names.length; i++) {
+ email = name = '';
+ item = $.trim(names[i]);
+
+ if (!item.length) {
+ continue;
+ } // address in brackets without name (do nothing)
+ else if (item.match(/^<[^@]+@[^>]+>$/)) {
+ email = item.replace(/[<>]/g, '');
+ } // address without brackets and without name (add brackets)
+ else if (rcube_check_email(item)) {
+ email = item;
+ } // address with name
+ else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
+ email = RegExp.$1;
+ name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
+ }
+ if (email) {
+ add_attendee($.extend({ email:email, name:name }, params));
+ success = true;
+ }
+ else {
+ alert(rcmail.gettext('noemailwarning'));
+ }
+ }
+
+ return success;
+ };
+
+ // add the given attendee to the list
+ var add_attendee = function(data, readonly, before)
+ {
+ if (!me.selected_event)
+ return false;
+
+ // check for dupes...
+ var exists = false;
+ $.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
+ if (exists)
+ return false;
+
+ var calendar = me.selected_event && me.calendars[me.selected_event.calendar] ? me.calendars[me.selected_event.calendar] : me.calendars[me.selected_calendar];
+
+ var dispname = Q(data.name || data.email);
+ if (data.email)
+ dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+
+ // role selection
+ var organizer = data.role == 'ORGANIZER';
+ var opts = {};
+ if (organizer)
+ opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer');
+ opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
+ opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
+ opts['NON-PARTICIPANT'] = rcmail.gettext('calendar.rolenonparticipant');
+
+ if (data.cutype != 'RESOURCE')
+ opts['CHAIR'] = rcmail.gettext('calendar.rolechair');
+
+ if (organizer && !readonly)
+ dispname = rcmail.env['identities-selector'];
+
+ var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','calendar') + '">';
+ for (var r in opts)
+ select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
+ select += '</select>';
+
+ // availability
+ var avail = data.email ? 'loading' : 'unknown';
+
+ // delete icon
+ var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
+ var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
+ var tooltip = data.status || '';
+
+ // send invitation checkbox
+ var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('calendar.sendinvitations')) + '" '
+ + (!data.noreply && settings.itip_notify & 1 ? 'checked="checked" ' : '') + '/>';
+
+ 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'];
+
+ // add expand button for groups
+ if (data.cutype == 'GROUP') {
+ dispname += ' <a href="#expand" data-email="' + Q(data.email) + '" class="iconbutton add expandlink" title="' + rcmail.gettext('expandattendeegroup','libcalendaring') + '">' +
+ rcmail.gettext('expandattendeegroup','libcalendaring') + '</a>';
+ }
+
+ var img_src = rcmail.assets_path('program/resources/blank.gif');
+ var html = '<td class="role">' + select + '</td>' +
+ '<td class="name"><span class="attendee-name">' + dispname + '</span></td>' +
+ '<td class="availability"><img src="' + img_src + '" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
+ '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
+ (data.cutype != 'RESOURCE' ? '<td class="invite">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
+ '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
+
+ var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
+ var tr = $('<tr>')
+ .addClass(String(data.role).toLowerCase())
+ .html(html);
+
+ if (before)
+ tr.insertBefore(before)
+ else
+ tr.appendTo(table);
+
+ tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
+ tr.find('a.mailtolink').click(event_attendee_click);
+ tr.find('a.expandlink').click(data, function(e) { me.expand_attendee_group(e, add_attendee, remove_attendee); return false; });
+ tr.find('input.edit-attendee-reply').click(function() {
+ var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
+ $('#eventedit .attendees-commentbox')[enabled ? 'show' : 'hide']();
+ });
+
+ // select organizer identity
+ if (data.identity_id)
+ $('#edit-identities-list').val(data.identity_id);
+
+ // check free-busy status
+ if (avail == 'loading') {
+ check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event);
+ }
+
+ event_attendees.push(data);
+ return true;
+ };
+
+ // iterate over all attendees and update their free-busy status display
+ var update_freebusy_status = function(event)
+ {
+ attendees_list.find('img.availabilityicon').each(function(i,v) {
+ var email, icon = $(this);
+ if (email = icon.attr('data-email'))
+ check_freebusy_status(icon, email, event);
+ });
+
+ freebusy_ui.needsupdate = false;
+ };
+
+ // load free-busy status from server and update icon accordingly
+ var check_freebusy_status = function(icon, email, event)
+ {
+ var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false };
+ if (!calendar.freebusy) {
+ $(icon).attr('class', 'availabilityicon unknown');
+ return;
+ }
+
+ icon = $(icon).attr('class', 'availabilityicon loading');
+
+ $.ajax({
+ type: 'GET',
+ dataType: 'html',
+ url: rcmail.url('freebusy-status'),
+ data: { email:email, start:date2servertime(clone_date(event.start, event.allDay?1:0)), end:date2servertime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
+ success: function(status){
+ var avail = String(status).toLowerCase();
+ icon.removeClass('loading').addClass(avail).attr('alt', rcmail.gettext('avail' + avail, 'calendar'));
+ },
+ error: function(){
+ icon.removeClass('loading').addClass('unknown').attr('alt', rcmail.gettext('availunknown', 'calendar'));
+ }
+ });
+ };
+
+ // remove an attendee from the list
+ var remove_attendee = function(elem, id)
+ {
+ $(elem).closest('tr').remove();
+ event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
+ };
+
+ // open a dialog to display detailed free-busy information and to find free slots
+ var event_resources_dialog = function(search)
+ {
+ var $dialog = $('#eventresourcesdialog');
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ // dialog buttons
+ var buttons = {};
+
+ buttons[rcmail.gettext('addresource', 'calendar')] = function() {
+ rcmail.command('add-resource');
+ };
+
+ buttons[rcmail.gettext('close')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('findresources', 'calendar'),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ resize: function(e) {
+ var container = $(rcmail.gui_objects.resourceinfocalendar);
+ container.fullCalendar('option', 'height', container.height() + 4);
+ },
+ buttons: buttons,
+ width: 900,
+ height: 500
+ }).show();
+
+ // define add-button as main action
+ $('.ui-dialog-buttonset .ui-button', $dialog.parent()).first().addClass('mainaction').attr('id', 'rcmbtncalresadd');
+
+ me.dialog_resize($dialog.get(0), 540, Math.min(1000, $(window).width() - 50));
+
+ // set search query
+ $('#resourcesearchbox').val(search || '');
+
+ // initialize the treelist widget
+ if (!resources_treelist) {
+ resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, {
+ id_prefix: 'rcres',
+ id_encode: rcmail.html_identifier_encode,
+ id_decode: rcmail.html_identifier_decode,
+ selectable: true,
+ save_state: true
+ });
+ resources_treelist.addEventListener('select', function(node) {
+ if (resources_data[node.id]) {
+ resource_showinfo(resources_data[node.id]);
+ rcmail.enable_command('add-resource', me.selected_event && $("#eventedit").is(':visible') ? true : false);
+ }
+ else {
+ rcmail.enable_command('add-resource', false);
+ $(rcmail.gui_objects.resourceinfo).hide();
+ $(rcmail.gui_objects.resourceownerinfo).hide();
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSource', resources_events_source);
+ }
+ });
+
+ // fetch (all) resource data from server
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_request('resources-list', {}, me.loading_lock);
+
+ // register button
+ rcmail.register_button('add-resource', 'rcmbtncalresadd', 'uibutton');
+
+ // initialize resource calendar display
+ var resource_cal = $(rcmail.gui_objects.resourceinfocalendar);
+ resource_cal.fullCalendar($.extend({}, fullcalendar_defaults, {
+ header: { left: '', center: '', right: '' },
+ height: resource_cal.height() + 4,
+ defaultView: 'agendaWeek',
+ eventSources: [],
+ slotMinutes: 60,
+ allDaySlot: false,
+ eventRender: function(event, element, view) {
+ var title = rcmail.get_label(event.status, 'calendar');
+ element.addClass('status-' + event.status);
+ element.find('.fc-event-head').hide();
+ element.find('.fc-event-title').text(title);
+ element.attr('aria-label', me.event_date_text(event, true) + ': ' + title);
+ }
+ }));
+
+ $('#resource-calendar-prev').click(function(){
+ resource_cal.fullCalendar('prev');
+ return false;
+ });
+ $('#resource-calendar-next').click(function(){
+ resource_cal.fullCalendar('next');
+ return false;
+ });
+ }
+ else if (search) {
+ resource_search();
+ }
+ else {
+ resource_render_list(resources_index);
+ }
+
+ if (me.selected_event && me.selected_event.start) {
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('gotoDate', me.selected_event.start);
+ }
+ };
+
+ // render the resource details UI box
+ var resource_showinfo = function(resource)
+ {
+ // inline function to render a resource attribute
+ function render_attrib(value) {
+ if (typeof value == 'boolean') {
+ return value ? rcmail.get_label('yes') : rcmail.get_label('no');
+ }
+
+ return value;
+ }
+
+ if (rcmail.gui_objects.resourceinfo) {
+ var tr, table = $(rcmail.gui_objects.resourceinfo).show().find('tbody').html(''),
+ attribs = $.extend({ name:resource.name }, resource.attributes||{})
+ attribs.description = resource.description;
+
+ for (var k in attribs) {
+ if (typeof attribs[k] == 'undefined')
+ continue;
+ table.append($('<tr>').addClass(k)
+ .append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
+ .append('<td class="value">' + text2html(render_attrib(attribs[k])) + '</td>')
+ );
+ }
+
+ $(rcmail.gui_objects.resourceownerinfo).hide();
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSource', resources_events_source);
+
+ if (resource.owner) {
+ // display cached data
+ if (resource_owners[resource.owner]) {
+ resource_owner_load(resource_owners[resource.owner]);
+ }
+ else {
+ // fetch owner data from server
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock);
+ }
+ }
+
+ // load resource calendar
+ resources_events_source.url = "./?_task=calendar&_action=resources-calendar&_id="+urlencode(resource.ID);
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('addEventSource', resources_events_source);
+ }
+ };
+
+ // callback from server for resource listing
+ var resource_data_load = function(data)
+ {
+ var resources_tree = {};
+
+ // store data by ID
+ $.each(data, function(i, rec) {
+ resources_data[rec.ID] = rec;
+
+ // assign parent-relations
+ if (rec.members) {
+ $.each(rec.members, function(j, m){
+ resources_tree[m] = rec.ID;
+ });
+ }
+ });
+
+ // walk the parent-child tree to determine the depth of each node
+ $.each(data, function(i, rec) {
+ rec._depth = 0;
+ if (resources_tree[rec.ID])
+ rec.parent_id = resources_tree[rec.ID];
+
+ var parent_id = resources_tree[rec.ID];
+ while (parent_id) {
+ rec._depth++;
+ parent_id = resources_tree[parent_id];
+ }
+ });
+
+ // sort by depth, collection and name
+ data.sort(function(a,b) {
+ var j = a._type == 'collection' ? 1 : 0,
+ k = b._type == 'collection' ? 1 : 0,
+ d = a._depth - b._depth;
+ if (!d) d = (k - j);
+ if (!d) d = b.name < a.name ? 1 : -1;
+ return d;
+ });
+
+ $.each(data, function(i, rec) {
+ resources_index.push(rec.ID);
+ });
+
+ // apply search filter...
+ if ($('#resourcesearchbox').val() != '')
+ resource_search();
+ else // ...or render full list
+ resource_render_list(resources_index);
+
+ rcmail.set_busy(false, null, me.loading_lock);
+ };
+
+ // renders the given list of resource records into the treelist
+ var resource_render_list = function(index) {
+ var rec, link;
+
+ resources_treelist.reset();
+
+ $.each(index, function(i, dn) {
+ if (rec = resources_data[dn]) {
+ link = $('<a>').attr('href', '#')
+ .attr('rel', rec.ID)
+ .html(Q(rec.name));
+
+ resources_treelist.insert({ id:rec.ID, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false);
+ }
+ });
+ };
+
+ // callback from server for owner information display
+ var resource_owner_load = function(data)
+ {
+ if (data) {
+ // cache this!
+ resource_owners[data.ID] = data;
+
+ var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html('');
+
+ for (var k in data) {
+ if (k == 'event' || k == 'ID')
+ continue;
+
+ table.append($('<tr>').addClass(k)
+ .append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
+ .append('<td class="value">' + text2html(data[k]) + '</td>')
+ );
+ }
+
+ table.parent().show();
+ }
+ }
+
+ // quick-filter the loaded resource data
+ var resource_search = function()
+ {
+ var dn, rec, dataset = [],
+ q = $('#resourcesearchbox').val().toLowerCase();
+
+ if (q.length && resources_data) {
+ // search by iterating over all resource records
+ for (dn in resources_data) {
+ rec = resources_data[dn];
+ if ((rec.name && String(rec.name).toLowerCase().indexOf(q) >= 0)
+ || (rec.email && String(rec.email).toLowerCase().indexOf(q) >= 0)
+ || (rec.description && String(rec.description).toLowerCase().indexOf(q) >= 0)
+ ) {
+ dataset.push(rec.ID);
+ }
+ }
+
+ resource_render_list(dataset);
+
+ // select single match
+ if (dataset.length == 1) {
+ resources_treelist.select(dataset[0]);
+ }
+ }
+ else {
+ $('#resourcesearchbox').val('');
+ }
+ };
+
+ //
+ var reset_resource_search = function()
+ {
+ $('#resourcesearchbox').val('').focus();
+ resource_render_list(resources_index);
+ };
+
+ //
+ var add_resource2event = function()
+ {
+ var resource = resources_data[resources_treelist.get_selection()];
+ if (resource) {
+ if (add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource)))
+ rcmail.display_message(rcmail.get_label('resourceadded', 'calendar'), 'confirmation');
+ }
+ }
+
+ // when the user accepts or declines an event invitation
+ var event_rsvp = function(response, delegate, replymode)
+ {
+ var btn;
+ if (typeof response == 'object') {
+ btn = $(response);
+ response = btn.attr('rel')
+ }
+ else {
+ btn = $('#event-rsvp input.button[rel='+response+']');
+ }
+
+ // show menu to select rsvp reply mode (current or all)
+ if (me.selected_event && me.selected_event.recurrence && !replymode) {
+ rcube_libcalendaring.itip_rsvp_recurring(btn, function(resp, mode) {
+ event_rsvp(resp, null, mode);
+ });
+ return;
+ }
+
+ if (me.selected_event && me.selected_event.attendees && response) {
+ // bring up delegation dialog
+ if (response == 'delegated' && !delegate) {
+ rcube_libcalendaring.itip_delegate_dialog(function(data) {
+ data.rsvp = data.rsvp ? 1 : '';
+ event_rsvp('delegated', data, replymode);
+ });
+ return;
+ }
+
+ // update attendee status
+ attendees = [];
+ for (var data, i=0; i < me.selected_event.attendees.length; i++) {
+ data = me.selected_event.attendees[i];
+ if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
+ data.status = response.toUpperCase();
+ data.rsvp = 0; // unset RSVP flag
+
+ if (data.status == 'DELEGATED') {
+ data['delegated-to'] = delegate.to;
+ data.rsvp = delegate.rsvp
+ }
+ else {
+ if (data['delegated-to']) {
+ delete data['delegated-to'];
+ if (data.role == 'NON-PARTICIPANT' && data.status != 'DECLINED')
+ data.role = 'REQ-PARTICIPANT';
+ }
+ }
+
+ attendees.push(i)
+ }
+ else if (response != 'DELEGATED' && data['delegated-from'] &&
+ settings.identity.emails.indexOf(';'+String(data['delegated-from']).toLowerCase()) >= 0) {
+ delete data['delegated-from'];
+ }
+
+ // set free_busy status to transparent if declined (#4425)
+ if (data.status == 'DECLINED' || data.role == 'NON-PARTICIPANT') {
+ me.selected_event.free_busy = 'free';
+ }
+ else {
+ me.selected_event.free_busy = 'busy';
+ }
+ }
+
+ // submit status change to server
+ var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val(), _savemode: replymode || 'all' }, (delegate || {})),
+ noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
+
+ // import event from mail (temporary iTip event)
+ if (submit_data._mbox && submit_data._uid) {
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('mailimportitip', {
+ _mbox: submit_data._mbox,
+ _uid: submit_data._uid,
+ _part: submit_data._part,
+ _status: response,
+ _to: (delegate ? delegate.to : null),
+ _rsvp: (delegate && delegate.rsvp) ? 1 : 0,
+ _noreply: noreply,
+ _comment: submit_data.comment,
+ _instance: submit_data._instance,
+ _savemode: submit_data._savemode
+ });
+ }
+ else if (settings.invitation_calendars) {
+ update_event('rsvp', submit_data, { status:response, noreply:noreply, attendees:attendees });
+ }
+ else {
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response, attendees:attendees, noreply:noreply });
+ }
+
+ event_show_dialog(me.selected_event);
+ }
+ };
+
+ // add the given date to the RDATE list
+ var add_rdate = function(date)
+ {
+ var li = $('<li>')
+ .attr('data-value', date2servertime(date))
+ .html('<span>' + Q($.fullCalendar.formatDate(date, settings['date_format'])) + '</span>')
+ .appendTo('#edit-recurrence-rdates');
+
+ $('<a>').attr('href', '#del')
+ .addClass('iconbutton delete')
+ .html(rcmail.get_label('delete', 'calendar'))
+ .attr('title', rcmail.get_label('delete', 'calendar'))
+ .appendTo(li);
+ };
+
+ // re-sort the list items by their 'data-value' attribute
+ var sort_rdates = function()
+ {
+ var mylist = $('#edit-recurrence-rdates'),
+ listitems = mylist.children('li').get();
+ listitems.sort(function(a, b) {
+ var compA = $(a).attr('data-value');
+ var compB = $(b).attr('data-value');
+ return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
+ })
+ $.each(listitems, function(idx, item) { mylist.append(item); });
+ }
+
+ // remove the link reference matching the given uri
+ function remove_link(elem)
+ {
+ var $elem = $(elem), uri = $elem.attr('data-uri');
+
+ me.selected_event.links = $.grep(me.selected_event.links, function(link) { return link.uri != uri; });
+
+ // remove UI list item
+ $elem.hide().closest('li').addClass('deleted');
+ }
+
+ // post the given event data to server
+ var update_event = function(action, data, add)
+ {
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('calendar/event', $.extend({ action:action, e:data }, (add || {})));
+
+ // render event temporarily into the calendar
+ if ((data.start && data.end) || data.id) {
+ var event = data.id ? $.extend(fc.fullCalendar('clientEvents', function(e){ return e.id == data.id; })[0], data) : data;
+ if (data.start)
+ event.start = data.start;
+ if (data.end)
+ event.end = data.end;
+ if (data.allday !== undefined)
+ event.allDay = data.allday;
+ event.editable = false;
+ event.temp = true;
+ event.className = 'fc-event-cal-'+data.calendar+' fc-event-temp';
+ fc.fullCalendar(data.id ? 'updateEvent' : 'renderEvent', event);
+
+ // mark all recurring instances as temp
+ if (event.recurrence || event.recurrence_id) {
+ var base_id = event.recurrence_id ? event.recurrence_id : String(event.id).replace(/-\d+(T\d{6})?$/, '');
+ $.each(fc.fullCalendar('clientEvents', function(e){ return e.id == base_id || e.recurrence_id == base_id; }), function(i,ev) {
+ ev.temp = true;
+ ev.editable = false;
+ event.className += ' fc-event-temp';
+ fc.fullCalendar('updateEvent', ev);
+ });
+ }
+ }
+ };
+
+ // mouse-click handler to check if the show dialog is still open and prevent default action
+ var dialog_check = function(e)
+ {
+ var showd = $("#eventshow");
+ if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length && !$(e.target).closest('.popupmenu').length) {
+ showd.dialog('close');
+ e.stopImmediatePropagation();
+ ignore_click = true;
+ return false;
+ }
+ else if (ignore_click) {
+ window.setTimeout(function(){ ignore_click = false; }, 20);
+ return false;
+ }
+ return true;
+ };
+
+ // display confirm dialog when modifying/deleting an event
+ var update_event_confirm = function(action, event, data)
+ {
+ if (!data) data = event;
+ var decline = false, notify = false, html = '', cal = me.calendars[event.calendar],
+ _has_attendees = has_attendees(event), _is_organizer = is_organizer(event);
+
+ // event has attendees, ask whether to notify them
+ if (_has_attendees) {
+ var checked = (settings.itip_notify & 1 ? ' checked="checked"' : '');
+ if (_is_organizer) {
+ notify = true;
+ if (settings.itip_notify & 2) {
+ html += '<div class="message">' +
+ '<label><input class="confirm-attendees-donotify" type="checkbox"' + checked + ' value="1" name="notify" />&nbsp;' +
+ rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
+ '</label></div>';
+ }
+ else {
+ data._notify = settings.itip_notify;
+ }
+ }
+ else if (action == 'remove' && is_attendee(event)) {
+ decline = true;
+ checked = event.status != 'CANCELLED' ? checked : '';
+ html += '<div class="message">' +
+ '<label><input class="confirm-attendees-decline" type="checkbox"' + checked + ' value="1" name="decline" />&nbsp;' +
+ rcmail.gettext('itipdeclineevent', 'calendar') +
+ '</label></div>';
+ }
+ else {
+ html += '<div class="message">' + rcmail.gettext('localchangeswarning', 'calendar') + '</div>';
+ }
+ }
+
+ // recurring event: user needs to select the savemode
+ if (event.recurrence) {
+ var future_disabled = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
+
+ // disable the 'future' savemode if I'm an attendee
+ // reason: no calendaring system supports the thisandfuture range parameter in iTip REPLY
+ if (action == 'remove' && _has_attendees && !_is_organizer && is_attendee(event)) {
+ future_disabled = ' disabled';
+ }
+
+ html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
+ rcmail.gettext(message_label, 'calendar') + '</div>' +
+ '<div class="savemode">' +
+ '<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
+ '<a href="#future" class="button' + future_disabled + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
+ '<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
+ (action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
+ '</div>';
+ }
+
+ // show dialog
+ if (html) {
+ var $dialog = $('<div>').html(html);
+
+ $dialog.find('a.button').button().filter(':not(.disabled)').click(function(e) {
+ data._savemode = String(this.href).replace(/.+#/, '');
+ data._notify = settings.itip_notify;
+
+ // open event edit dialog when saving as new
+ if (data._savemode == 'new') {
+ event._savemode = 'new';
+ event_edit_dialog('edit', event);
+ fc.fullCalendar('refetchEvents');
+ }
+ else {
+ if ($dialog.find('input.confirm-attendees-donotify').length)
+ data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
+ if (decline) {
+ data._decline = $dialog.find('input.confirm-attendees-decline:checked').length;
+ data._notify = 0;
+ }
+ update_event(action, data);
+ }
+
+ $dialog.dialog("close");
+ return false;
+ });
+
+ var buttons = [];
+
+ if (!event.recurrence) {
+ buttons.push({
+ text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
+ click: function() {
+ data._notify = notify && $dialog.find('input.confirm-attendees-donotify:checked').length ? 1 : 0;
+ data._decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
+ update_event(action, data);
+ $(this).dialog("close");
+ }
+ });
+ }
+
+ buttons.push({
+ text: rcmail.gettext('cancel', 'calendar'),
+ click: function() {
+ $(this).dialog("close");
+ }
+ });
+
+ $dialog.dialog({
+ modal: true,
+ width: 460,
+ dialogClass: 'warning',
+ title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
+ buttons: buttons,
+ open: function() {
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function(){
+ $dialog.dialog("destroy").remove();
+ if (!rcmail.busy)
+ fc.fullCalendar('refetchEvents');
+ }
+ }).addClass('event-update-confirm').show();
+
+ return false;
+ }
+ // show regular confirm box when deleting
+ else if (action == 'remove' && !cal.undelete) {
+ if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar')))
+ return false;
+ }
+
+ // do update
+ update_event(action, data);
+
+ return true;
+ };
+
+ var update_agenda_toolbar = function()
+ {
+ $('#agenda-listrange').val(fc.fullCalendar('option', 'listRange'));
+ $('#agenda-listsections').val(fc.fullCalendar('option', 'listSections'));
+ }
+
+
+ /*** public methods ***/
+
+ /**
+ * Remove saving lock and free the UI for new input
+ */
+ this.unlock_saving = function()
+ {
+ if (me.saving_lock)
+ rcmail.set_busy(false, null, me.saving_lock);
+ };
+
+ // opens calendar day-view in a popup
+ this.fisheye_view = function(date)
+ {
+ $('#fish-eye-view:ui-dialog').dialog('close');
+
+ // create list of active event sources
+ var src, cals = {}, sources = [];
+ for (var id in this.calendars) {
+ src = $.extend({}, this.calendars[id]);
+ src.editable = false;
+ src.url = null;
+ src.events = [];
+
+ if (src.active) {
+ cals[id] = src;
+ sources.push(src);
+ }
+ }
+
+ // copy events already loaded
+ var events = fc.fullCalendar('clientEvents');
+ for (var event, i=0; i< events.length; i++) {
+ event = events[i];
+ if (event.source && (src = cals[event.source.id])) {
+ src.events.push(event);
+ }
+ }
+
+ var h = $(window).height() - 50;
+ var dialog = $('<div>')
+ .attr('id', 'fish-eye-view')
+ .dialog({
+ modal: true,
+ width: 680,
+ height: h,
+ title: $.fullCalendar.formatDate(date, 'dddd ' + settings['date_long']),
+ close: function(){
+ dialog.dialog("destroy");
+ me.fisheye_date = null;
+ }
+ })
+ .fullCalendar($.extend({}, fullcalendar_defaults, {
+ defaultView: 'agendaDay',
+ header: { left: '', center: '', right: '' },
+ height: h - 50,
+ date: date.getDate(),
+ month: date.getMonth(),
+ year: date.getFullYear(),
+ eventSources: sources
+ }));
+
+ this.fisheye_date = date;
+ };
+
+ // opens the given calendar in a popup dialog
+ this.quickview = function(id, shift)
+ {
+ var src, in_quickview = false;
+ $.each(this.quickview_sources, function(i,cal) {
+ if (cal.id == id) {
+ in_quickview = true;
+ src = cal;
+ }
+ });
+
+ // remove source from quickview
+ if (in_quickview && shift) {
+ this.quickview_sources = $.grep(this.quickview_sources, function(src) { return src.id != id; });
+ }
+ else {
+ if (!shift) {
+ // remove all current quickview event sources
+ if (this.quickview_active) {
+ fc.fullCalendar('removeEventSources');
+ }
+
+ this.quickview_sources = [];
+
+ // uncheck all active quickview icons
+ calendars_list.container.find('div.focusview')
+ .add('#calendars .searchresults div.focusview')
+ .removeClass('focusview')
+ .find('a.quickview').attr('aria-checked', 'false');
+ }
+
+ if (!in_quickview) {
+ // clone and modify calendar properties
+ src = $.extend({}, this.calendars[id]);
+ src.url += '&_quickview=1';
+ this.quickview_sources.push(src);
+ }
+ }
+
+ // disable quickview
+ if (this.quickview_active && !this.quickview_sources.length) {
+ // register regular calendar event sources
+ $.each(this.calendars, function(k, cal) {
+ if (cal.active)
+ fc.fullCalendar('addEventSource', cal);
+ });
+
+ this.quickview_active = false;
+ $('body').removeClass('quickview-active');
+
+ // uncheck all active quickview icons
+ calendars_list.container.find('div.focusview')
+ .add('#calendars .searchresults div.focusview')
+ .removeClass('focusview')
+ .find('a.quickview').attr('aria-checked', 'false');
+ }
+ // activate quickview
+ else if (!this.quickview_active) {
+ // remove regular calendar event sources
+ fc.fullCalendar('removeEventSources');
+
+ // register quickview event sources
+ $.each(this.quickview_sources, function(i, src) {
+ fc.fullCalendar('addEventSource', src);
+ });
+
+ this.quickview_active = true;
+ $('body').addClass('quickview-active');
+ }
+ // update quickview sources
+ else if (in_quickview) {
+ fc.fullCalendar('removeEventSource', src);
+ }
+ else if (src) {
+ fc.fullCalendar('addEventSource', src);
+ }
+
+ // activate quickview icon
+ if (this.quickview_active) {
+ $(calendars_list.get_item(id)).find('.calendar').first()
+ .add('#calendars .searchresults .cal-' + id)
+ [in_quickview ? 'removeClass' : 'addClass']('focusview')
+ .find('a.quickview').attr('aria-checked', in_quickview ? 'false' : 'true');
+ }
+ };
+
+ // disable quickview mode
+ function reset_quickview()
+ {
+ // remove all current quickview event sources
+ if (me.quickview_active) {
+ fc.fullCalendar('removeEventSources');
+ me.quickview_sources = [];
+ }
+
+ // register regular calendar event sources
+ $.each(me.calendars, function(k, cal) {
+ if (cal.active)
+ fc.fullCalendar('addEventSource', cal);
+ });
+
+ // uncheck all active quickview icons
+ calendars_list.container.find('div.focusview')
+ .add('#calendars .searchresults div.focusview')
+ .removeClass('focusview')
+ .find('a.quickview').attr('aria-checked', 'false');
+
+ me.quickview_active = false;
+ $('body').removeClass('quickview-active');
+ };
+
+ //public method to show the print dialog.
+ this.print_calendars = function(view)
+ {
+ if (!view) view = fc.fullCalendar('getView').name;
+ var date = fc.fullCalendar('getDate') || new Date();
+ var range = fc.fullCalendar('option', 'listRange');
+ var sections = fc.fullCalendar('option', 'listSections');
+ rcmail.open_window(rcmail.url('print', { view: view, date: date2unixtime(date), range: range, sections: sections, search: this.search_query }), true, true);
+ };
+
+ // public method to bring up the new event dialog
+ this.add_event = function(templ) {
+ if (this.selected_calendar) {
+ var now = new Date();
+ var date = fc.fullCalendar('getDate');
+ if (typeof date != 'Date')
+ date = now;
+ date.setHours(now.getHours()+1);
+ date.setMinutes(0);
+ var end = new Date(date.getTime());
+ end.setHours(date.getHours()+1);
+ event_edit_dialog('new', $.extend({ start:date, end:end, allDay:false, calendar:this.selected_calendar }, templ || {}));
+ }
+ };
+
+ // delete the given event after showing a confirmation dialog
+ this.delete_event = function(event) {
+ // show confirm dialog for recurring events, use jquery UI dialog
+ return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees });
+ };
+
+ // opens a jquery UI dialog with event properties (or empty for creating a new calendar)
+ this.calendar_edit_dialog = function(calendar)
+ {
+ // close show dialog first
+ var $dialog = $("#calendarform");
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (!calendar)
+ calendar = { name:'', color:'cc0000', editable:true, showalarms:true };
+
+ var form, name, color, alarms;
+
+ $dialog.html(rcmail.get_label('loading'));
+ $.ajax({
+ type: 'GET',
+ dataType: 'html',
+ url: rcmail.url('calendar'),
+ data: { action:(calendar.id ? 'form-edit' : 'form-new'), c:{ id:calendar.id }, driver: calendar.driver },
+ success: function(data) {
+ $dialog.html(data);
+ // resize and reposition dialog window
+ form = $('#calendarpropform');
+ me.dialog_resize('#calendarform', form.height(), form.width());
+ name = $('#calendar-name').prop('disabled', !calendar.editable && !calendar.editable_name).val(calendar.editname || calendar.name);
+ color = $('#calendar-color').val(calendar.color).miniColors({ value: calendar.color, colorValues:rcmail.env.mscolors });
+ alarms = $('#calendar-showalarms').prop('checked', calendar.showalarms).get(0);
+ name.select();
+ }
+ });
+
+ // dialog buttons
+ var buttons = {};
+
+ buttons[rcmail.gettext('save', 'calendar')] = function() {
+ // form is not loaded
+ if (!form || !form.length)
+ return;
+
+ // TODO: do some input validation
+ if (!name.val() || name.val().length < 2) {
+ alert(rcmail.gettext('invalidcalendarproperties', 'calendar'));
+ name.select();
+ return;
+ }
+
+ // post data to server
+ var data = form.serializeJSON();
+ if (data.color)
+ data.color = data.color.replace(/^#/, '');
+ if (calendar.id)
+ data.id = calendar.id;
+ if (alarms)
+ data.showalarms = alarms.checked ? 1 : 0;
+
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data, driver: calendar.driver });
+ $dialog.dialog("close");
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: true,
+ closeOnEscape: false,
+ title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'),
+ open: function() {
+ $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+ },
+ close: function() {
+ $dialog.html('').dialog("destroy").hide();
+ },
+ buttons: buttons,
+ minWidth: 400,
+ width: 420
+ }).show();
+
+ };
+
+ this.calendar_remove = function(calendar)
+ {
+ this.calendar_destroy_source(calendar.id);
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:calendar.id, active:0, permanent:0, recursive:1 } });
+ return true;
+ };
+
+ this.calendar_delete = function(calendar)
+ {
+ if (confirm(rcmail.gettext(calendar.children ? 'deletecalendarconfirmrecursive' : 'deletecalendarconfirm', 'calendar'))) {
+ rcmail.http_post('calendar', { action:'delete', c:{ id:calendar.id }, driver: calendar.driver });
+ return true;
+ }
+ return false;
+ };
+
+ this.calendar_destroy_source = function(id)
+ {
+ var delete_ids = [];
+
+ if (this.calendars[id]) {
+ // find sub-calendars
+ if (this.calendars[id].children) {
+ for (var child_id in this.calendars) {
+ if (String(child_id).indexOf(id) == 0)
+ delete_ids.push(child_id);
+ }
+ }
+ else {
+ delete_ids.push(id);
+ }
+ }
+
+ // delete all calendars in the list
+ for (var i=0; i < delete_ids.length; i++) {
+ id = delete_ids[i];
+ calendars_list.remove(id);
+ fc.fullCalendar('removeEventSource', this.calendars[id]);
+ $('#edit-calendar option[value="'+id+'"]').remove();
+ delete this.calendars[id];
+ }
+ };
+
+ // open a dialog to upload an .ics file with events to be imported
+ this.import_events = function(calendar)
+ {
+ // close show dialog first
+ var $dialog = $("#eventsimport"),
+ form = rcmail.gui_objects.importform;
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (calendar)
+ $('#event-import-calendar').val(calendar.id);
+
+ var buttons = {};
+ buttons[rcmail.gettext('import', 'calendar')] = function() {
+ if (form && form.elements._data.value) {
+ rcmail.async_upload_form(form, 'import_events', function(e) {
+ rcmail.set_busy(false, null, me.saving_lock);
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
+
+ // display error message if no sophisticated response from server arrived (e.g. iframe load error)
+ if (me.import_succeeded === null)
+ rcmail.display_message(rcmail.get_label('importerror', 'calendar'), 'error');
+ });
+
+ // display upload indicator (with extended timeout)
+ var timeout = rcmail.env.request_timeout;
+ rcmail.env.request_timeout = 600;
+ me.import_succeeded = null;
+ me.saving_lock = rcmail.set_busy(true, 'uploading');
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('disable');
+
+ // restore settings
+ rcmail.env.request_timeout = timeout;
+ }
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: false,
+ closeOnEscape: false,
+ title: rcmail.gettext('importevents', 'calendar'),
+ open: function() {
+ $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+ },
+ close: function() {
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
+ $dialog.dialog("destroy").hide();
+ },
+ buttons: buttons,
+ width: 520
+ }).show();
+
+ };
+
+ // callback from server if import succeeded
+ this.import_success = function(p)
+ {
+ this.import_succeeded = true;
+ $("#eventsimport:ui-dialog").dialog('close');
+ rcmail.set_busy(false, null, me.saving_lock);
+ rcmail.gui_objects.importform.reset();
+
+ if (p.refetch)
+ this.refresh(p);
+ };
+
+ // callback from server to report errors on import
+ this.import_error = function(p)
+ {
+ this.import_succeeded = false;
+ rcmail.set_busy(false, null, me.saving_lock);
+ rcmail.display_message(p.message || rcmail.get_label('importerror', 'calendar'), 'error');
+ }
+
+ // open a dialog to select calendars for export
+ this.export_events = function(calendar)
+ {
+ // close show dialog first
+ var $dialog = $("#eventsexport"),
+ form = rcmail.gui_objects.exportform;
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (calendar)
+ $('#event-export-calendar').val(calendar.id);
+
+ $('#event-export-range').change(function(e){
+ var custom = $('option:selected', this).val() == 'custom',
+ input = $('#event-export-startdate')
+ input.parent()[(custom?'show':'hide')]();
+ if (custom)
+ input.select();
+ })
+
+ var buttons = {};
+ buttons[rcmail.gettext('export', 'calendar')] = function() {
+ if (form) {
+ var start = 0, range = $('#event-export-range option:selected', this).val(),
+ source = $('#event-export-calendar option:selected').val(),
+ attachmt = $('#event-export-attachments').get(0).checked;
+
+ if (range == 'custom')
+ start = date2unixtime(parse_datetime('00:00', $('#event-export-startdate').val()));
+ else if (range > 0)
+ start = 'today -' + range + ' months';
+
+ rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 });
+ }
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: false,
+ closeOnEscape: false,
+ title: rcmail.gettext('exporttitle', 'calendar'),
+ open: function() {
+ $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+ },
+ close: function() {
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
+ $dialog.dialog("destroy").hide();
+ },
+ buttons: buttons,
+ width: 520
+ }).show();
+
+ };
+
+ // download the selected event as iCal
+ this.event_download = function(event)
+ {
+ if (event && event.id) {
+ rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 });
+ }
+ };
+
+ // open the message compose step with a calendar_event parameter referencing the selected event.
+ // the server-side plugin hook will pick that up and attach the event to the message.
+ this.event_sendbymail = function(event, e)
+ {
+ if (event && event.id) {
+ rcmail.command('compose', { _calendar_event:event._id }, e ? e.target : null, e);
+ }
+ };
+
+ // show URL of the given calendar in a dialog box
+ this.showurl = function(calendar)
+ {
+ var $dialog = $('#calendarurlbox');
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (calendar.feedurl) {
+ if (calendar.caldavurl) {
+ $('#caldavurl').val(calendar.caldavurl);
+ $('#calendarcaldavurl').show();
+ }
+ else {
+ $('#calendarcaldavurl').hide();
+ }
+
+ $dialog.dialog({
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('showurl', 'calendar'),
+ close: function() {
+ $dialog.dialog("destroy").hide();
+ },
+ width: 520
+ }).show();
+
+ $('#calfeedurl').val(calendar.feedurl).select();
+ }
+ };
+
+ // refresh the calendar view after saving event data
+ this.refresh = function(p)
+ {
+ var source = me.calendars[p.source];
+
+ // helper function to update the given fullcalendar view
+ function update_view(view, event, source) {
+ var existing = view.fullCalendar('clientEvents', event._id);
+ if (existing.length) {
+ $.extend(existing[0], event);
+ view.fullCalendar('updateEvent', existing[0]);
+ // remove old recurrence instances
+ if (event.recurrence && !event.recurrence_id)
+ view.fullCalendar('removeEvents', function(e){ return e._id.indexOf(event._id+'-') == 0; });
+ }
+ else {
+ event.source = source; // link with source
+ view.fullCalendar('renderEvent', event);
+ }
+ }
+
+ // remove temp events
+ fc.fullCalendar('removeEvents', function(e){ return e.temp; });
+
+ if (source && (p.refetch || (p.update && !source.active))) {
+ // activate event source if new event was added to an invisible calendar
+ if (this.quickview_active) {
+ // map source to the quickview_sources equivalent
+ $.each(this.quickview_sources, function(src) {
+ if (src.id == source.id) {
+ source = src;
+ return false;
+ }
+ });
+ fc.fullCalendar('refetchEvents', source, true);
+ }
+ else if (!source.active) {
+ source.active = true;
+ fc.fullCalendar('addEventSource', source);
+ $('#rcmlical' + source.id + ' input').prop('checked', true);
+ }
+ else
+ fc.fullCalendar('refetchEvents', source, true);
+
+ fetch_counts();
+ }
+ // add/update single event object
+ else if (source && p.update) {
+ var event = p.update;
+ event.temp = false;
+ event.editable = 0;
+
+ // update fish-eye view
+ if (this.fisheye_date)
+ update_view($('#fish-eye-view'), event, source);
+
+ // update main view
+ event.editable = source.editable;
+ update_view(fc, event, source);
+
+ // update the currently displayed event dialog
+ if ($('#eventshow').is(':visible') && me.selected_event && me.selected_event.id == event.id)
+ event_show_dialog(event)
+ }
+ // refetch all calendars
+ else if (p.refetch) {
+ fc.fullCalendar('refetchEvents', undefined, true);
+ fetch_counts();
+ }
+ };
+
+ // modify query parameters for refresh requests
+ this.before_refresh = function(query)
+ {
+ var view = fc.fullCalendar('getView');
+
+ query.start = date2unixtime(view.visStart);
+ query.end = date2unixtime(view.visEnd);
+
+ if (this.search_query)
+ query.q = this.search_query;
+
+ return query;
+ };
+
+ // callback from server providing event counts
+ this.update_counts = function(p)
+ {
+ $.each(p.counts, function(cal, count) {
+ var li = calendars_list.get_item(cal),
+ bubble = $(li).children('.calendar').find('span.count');
+
+ if (!bubble.length && count > 0) {
+ bubble = $('<span>')
+ .addClass('count')
+ .appendTo($(li).children('.calendar').first())
+ }
+
+ if (count > 0) {
+ bubble.text(count).show();
+ }
+ else {
+ bubble.text('').hide();
+ }
+ });
+ };
+
+ // callback after an iTip message event was imported
+ this.itip_message_processed = function(data)
+ {
+ // remove temporary iTip source
+ fc.fullCalendar('removeEventSource', this.calendars['--invitation--itip']);
+
+ $('#eventshow:ui-dialog').dialog('close');
+ this.selected_event = null;
+
+ // refresh destination calendar source
+ this.refresh({ source:data.calendar, refetch:true });
+
+ this.unlock_saving();
+
+ // process 'after_action' in mail task
+ if (window.opener && window.opener.rcube_libcalendaring)
+ window.opener.rcube_libcalendaring.itip_message_processed(data);
+ };
+
+ // reload the calendar view by keeping the current date/view selection
+ this.reload_view = function()
+ {
+ var query = { view: fc.fullCalendar('getView').name },
+ date = fc.fullCalendar('getDate');
+ if (date)
+ query.date = date2unixtime(date);
+ rcmail.redirect(rcmail.url('', query));
+ }
+
+ // update browser location to remember current view
+ this.update_state = function()
+ {
+ var query = { view: current_view },
+ date = fc.fullCalendar('getDate');
+ if (date)
+ query.date = date2unixtime(date);
+
+ if (window.history.replaceState)
+ window.history.replaceState({}, document.title, rcmail.url('', query).replace('&_action=', ''));
+ };
+
+ this.resource_search = resource_search;
+ this.reset_resource_search = reset_resource_search;
+ this.add_resource2event = add_resource2event;
+ this.resource_data_load = resource_data_load;
+ this.resource_owner_load = resource_owner_load;
+
+
+ /*** event searching ***/
+
+ // execute search
+ this.quicksearch = function()
+ {
+ if (rcmail.gui_objects.qsearchbox) {
+ var q = rcmail.gui_objects.qsearchbox.value;
+ if (q != '') {
+ var id = 'search-'+q;
+ var sources = [];
+
+ if (me.quickview_active)
+ reset_quickview();
+
+ if (this._search_message)
+ rcmail.hide_message(this._search_message);
+
+ for (var sid in this.calendars) {
+ if (this.calendars[sid]) {
+ this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '') + '&q=' + urlencode(q);
+ sources.push(sid);
+ }
+ }
+ id += '@'+sources.join(',');
+
+ // ignore if query didn't change
+ if (this.search_request == id) {
+ return;
+ }
+ // remember current view
+ else if (!this.search_request) {
+ this.default_view = fc.fullCalendar('getView').name;
+ }
+
+ this.search_request = id;
+ this.search_query = q;
+
+ // change to list view
+ fc.fullCalendar('option', 'listSections', 'month')
+ .fullCalendar('option', 'listRange', Math.max(60, settings['agenda_range']))
+ .fullCalendar('changeView', 'table');
+
+ update_agenda_toolbar();
+
+ // refetch events with new url (if not already triggered by changeView)
+ if (!this.is_loading)
+ fc.fullCalendar('refetchEvents');
+ }
+ else // empty search input equals reset
+ this.reset_quicksearch();
+ }
+ };
+
+ // reset search and get back to normal event listing
+ this.reset_quicksearch = function()
+ {
+ $(rcmail.gui_objects.qsearchbox).val('');
+
+ if (this._search_message)
+ rcmail.hide_message(this._search_message);
+
+ if (this.search_request) {
+ // hide bottom links of agenda view
+ fc.find('.fc-list-content > .fc-listappend').hide();
+
+ // restore original event sources and view mode from fullcalendar
+ fc.fullCalendar('option', 'listSections', settings['agenda_sections'])
+ .fullCalendar('option', 'listRange', settings['agenda_range']);
+
+ update_agenda_toolbar();
+
+ for (var sid in this.calendars) {
+ if (this.calendars[sid])
+ this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '');
+ }
+ if (this.default_view)
+ fc.fullCalendar('changeView', this.default_view);
+
+ if (!this.is_loading)
+ fc.fullCalendar('refetchEvents');
+
+ this.search_request = this.search_query = null;
+ }
+ };
+
+ // callback if all sources have been fetched from server
+ this.events_loaded = function(count)
+ {
+ var addlinks, append = '';
+
+ // enhance list view when searching
+ if (this.search_request) {
+ if (!count) {
+ this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice');
+ append = '<div class="message">' + rcmail.gettext('searchnoresults', 'calendar') + '</div>';
+ }
+ append += '<div class="fc-bottomlinks formlinks"></div>';
+ addlinks = true;
+ }
+
+ if (fc.fullCalendar('getView').name == 'table') {
+ var container = fc.find('.fc-list-content > .fc-listappend');
+ if (append) {
+ if (!container.length)
+ container = $('<div class="fc-listappend"></div>').appendTo(fc.find('.fc-list-content'));
+ container.html(append).show();
+ }
+ else if (container.length)
+ container.hide();
+
+ // add links to adjust search date range
+ if (addlinks) {
+ var lc = container.find('.fc-bottomlinks');
+ $('<a>').attr('href', '#').html(rcmail.gettext('searchearlierdates', 'calendar')).appendTo(lc).click(function(){
+ fc.fullCalendar('incrementDate', 0, -1, 0);
+ });
+ lc.append(" ");
+ $('<a>').attr('href', '#').html(rcmail.gettext('searchlaterdates', 'calendar')).appendTo(lc).click(function(){
+ var range = fc.fullCalendar('option', 'listRange');
+ if (range < 90) {
+ fc.fullCalendar('option', 'listRange', fc.fullCalendar('option', 'listRange') + 30).fullCalendar('render');
+ update_agenda_toolbar();
+ }
+ else
+ fc.fullCalendar('incrementDate', 0, 1, 0);
+ });
+ }
+ }
+
+ if (this.fisheye_date)
+ this.fisheye_view(this.fisheye_date);
+ };
+
+ // resize and reposition (center) the dialog window
+ this.dialog_resize = function(id, height, width)
+ {
+ var win = $(window), w = win.width(), h = win.height();
+ $(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) })
+ .dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?)
+ };
+
+ // adjust calendar view size
+ this.view_resize = function()
+ {
+ var footer = fc.fullCalendar('getView').name == 'table' ? $('#agendaoptions').outerHeight() : 0;
+ fc.fullCalendar('option', 'height', $('#calendar').height() - footer);
+ };
+
+ // mark the given calendar folder as selected
+ this.select_calendar = function(id, nolistupdate)
+ {
+ if (!nolistupdate)
+ calendars_list.select(id);
+
+ // trigger event hook
+ rcmail.triggerEvent('selectfolder', { folder:id, prefix:'rcmlical' });
+
+ this.selected_calendar = id;
+ };
+
+ // register the given calendar to the current view
+ var add_calendar_source = function(cal)
+ {
+ var color, brightness, select, id = cal.id;
+
+ me.calendars[id] = $.extend({
+ url: rcmail.url('calendar/load_events', { source: id, driver: cal.driver }),
+ editable: !cal.readonly,
+ className: 'fc-event-cal-'+id,
+ id: id
+ }, cal);
+
+ // choose black text color when background is bright, white otherwise
+ if (color = settings.event_coloring % 2 ? '' : '#' + cal.color) {
+ if (/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.test(color)) {
+ // use information about brightness calculation found at
+ // http://javascriptrules.com/2009/08/05/css-color-brightness-contrast-using-javascript/
+ brightness = (parseInt(RegExp.$1, 16) * 299 + parseInt(RegExp.$2, 16) * 587 + parseInt(RegExp.$3, 16) * 114) / 1000;
+ if (brightness > 125)
+ me.calendars[id].textColor = 'black';
+ }
+
+ me.calendars[id].color = color;
+ }
+
+ if (fc && (cal.active || cal.subscribed)) {
+ if (cal.active)
+ fc.fullCalendar('addEventSource', me.calendars[id]);
+
+ var submit = { id: id, active: cal.active ? 1 : 0 };
+ if (cal.subscribed !== undefined)
+ submit.permanent = cal.subscribed ? 1 : 0;
+ rcmail.http_post('calendar', { action:'subscribe', c:submit });
+ }
+
+ // insert to #calendar-select options if writeable
+ select = $('#edit-calendar');
+ if (fc && has_permission(cal, 'i') && select.length && !select.find('option[value="'+id+'"]').length) {
+ $('<option>').attr('value', id).html(cal.name).appendTo(select);
+ }
+ }
+
+ // fetch counts for some calendars from the server
+ var fetch_counts = function()
+ {
+ if (count_sources.length) {
+ setTimeout(function() {
+ rcmail.http_request('calendar/count', { source:count_sources });
+ }, 500);
+ }
+ };
+
+
+ /*** startup code ***/
+
+ // create list of event sources AKA calendars
+ var id, cal, active, event_sources = [];
+ for (id in rcmail.env.calendars) {
+ cal = rcmail.env.calendars[id];
+ active = cal.active || false;
+ add_calendar_source(cal);
+
+ // check active calendars
+ $('#rcmlical'+id+' > .calendar input').prop('checked', active);
+
+ if (active) {
+ event_sources.push(this.calendars[id]);
+ }
+ if (cal.counts) {
+ count_sources.push(id);
+ }
+
+ if (cal.editable && !this.selected_calendar) {
+ this.selected_calendar = id;
+ rcmail.enable_command('addevent', true);
+ }
+ }
+
+ // initialize treelist widget that controls the calendars list
+ var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+ calendars_list = new widget_class(rcmail.gui_objects.calendarslist, {
+ id_prefix: 'rcmlical',
+ selectable: true,
+ save_state: true,
+ keyboard: false,
+ searchbox: '#calendarlistsearch',
+ search_action: 'calendar/calendar',
+ search_sources: [ 'folders', 'users' ],
+ search_title: rcmail.gettext('calsearchresults','calendar')
+ });
+ calendars_list.addEventListener('select', function(node) {
+ if (node && node.id && me.calendars[node.id]) {
+ me.select_calendar(node.id, true);
+ rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
+ rcmail.enable_command('calendar-delete', me.calendars[node.id] && (me.calendars[node.id].deletable || !me.calendars[node.id].readonly));
+ rcmail.enable_command('calendar-remove', me.calendars[node.id] && me.calendars[node.id].removable);
+ }
+ });
+ calendars_list.addEventListener('insert-item', function(p) {
+ var cal = p.data;
+ if (cal && cal.id) {
+ add_calendar_source(cal);
+
+ // add css classes related to this calendar to document
+ if (cal.css) {
+ $('<style type="text/css"></style>')
+ .html(cal.css)
+ .appendTo('head');
+ }
+ }
+ });
+ calendars_list.addEventListener('subscribe', function(p) {
+ var cal;
+ if ((cal = me.calendars[p.id])) {
+ cal.subscribed = p.subscribed || false;
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:p.id, active:cal.active?1:0, permanent:cal.subscribed?1:0 } });
+ }
+ });
+ calendars_list.addEventListener('remove', function(p) {
+ if (me.calendars[p.id] && me.calendars[p.id].removable) {
+ me.calendar_remove(me.calendars[p.id]);
+ }
+ });
+ calendars_list.addEventListener('search-complete', function(data) {
+ if (data.length)
+ rcmail.display_message(rcmail.gettext('nrcalendarsfound','calendar').replace('$nr', data.length), 'voice');
+ else
+ rcmail.display_message(rcmail.gettext('nocalendarsfound','calendar'), 'info');
+ });
+ calendars_list.addEventListener('click-item', function(event) {
+ // handle clicks on quickview icon: temprarily add this source and open in quickview
+ if ($(event.target).hasClass('quickview') && event.data) {
+ if (!me.calendars[event.data.id]) {
+ event.data.readonly = true;
+ event.data.active = false;
+ event.data.subscribed = false;
+ add_calendar_source(event.data);
+ }
+ me.quickview(event.data.id, event.shiftKey || event.metaKey || event.ctrlKey);
+ return false;
+ }
+ });
+
+ // init (delegate) event handler on calendar list checkboxes
+ $(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e) {
+ e.stopPropagation();
+
+ if (me.quickview_active) {
+ this.checked = !this.checked;
+ return false;
+ }
+
+ var id = this.value;
+ if (me.calendars[id]) { // add or remove event source on click
+ var action;
+ if (this.checked) {
+ action = 'addEventSource';
+ me.calendars[id].active = true;
+ }
+ else {
+ action = 'removeEventSource';
+ me.calendars[id].active = false;
+ }
+
+ // adjust checked state of original list item
+ if (calendars_list.is_search()) {
+ calendars_list.container.find('input[value="'+id+'"]').prop('checked', this.checked);
+ }
+
+ // add/remove event source
+ fc.fullCalendar(action, me.calendars[id]);
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
+ }
+ })
+ .on('keypress', 'input[type=checkbox]', function(e) {
+ // select calendar on <Enter>
+ if (e.keyCode == 13) {
+ calendars_list.select(this.value);
+ return rcube_event.cancel(e);
+ }
+ })
+ // init (delegate) event handler on quickview links
+ .on('click', 'a.quickview', function(e) {
+ var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
+
+ if (calendars_list.is_search())
+ id = id.replace(/--xsR$/, '');
+
+ if (me.calendars[id])
+ me.quickview(id, e.shiftKey || e.metaKey || e.ctrlKey);
+
+ if (!rcube_event.is_keyboard(e) && this.blur)
+ this.blur();
+
+ e.stopPropagation();
+ return false;
+ });
+
+ // register dbl-click handler to open calendar edit dialog
+ $(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e){
+ var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
+ me.calendar_edit_dialog(me.calendars[id]);
+ });
+
+ // select default calendar
+ if (settings.default_calendar && this.calendars[settings.default_calendar] && this.calendars[settings.default_calendar].editable)
+ this.selected_calendar = settings.default_calendar;
+
+ if (this.selected_calendar)
+ this.select_calendar(this.selected_calendar);
+
+ var viewdate = new Date();
+ if (rcmail.env.date)
+ viewdate.setTime(fromunixtime(rcmail.env.date));
+
+ // add source with iTip event data for rendering
+ if (rcmail.env.itip_events && rcmail.env.itip_events.length) {
+ me.calendars['--invitation--itip'] = {
+ events: rcmail.env.itip_events,
+ className: 'fc-event-cal---invitation--itip',
+ color: '#fff',
+ textColor: '#333',
+ editable: false,
+ rights: 'lrs',
+ attendees: true
+ };
+ event_sources.push(me.calendars['--invitation--itip']);
+ }
+
+ // initalize the fullCalendar plugin
+ var fc = $('#calendar').fullCalendar($.extend({}, fullcalendar_defaults, {
+ header: {
+ right: 'prev,next today',
+ center: 'title',
+ left: 'agendaDay,agendaWeek,month,table'
+ },
+ date: viewdate.getDate(),
+ month: viewdate.getMonth(),
+ year: viewdate.getFullYear(),
+ height: $('#calendar').height(),
+ eventSources: event_sources,
+ selectable: true,
+ selectHelper: false,
+ loading: function(isLoading) {
+ me.is_loading = isLoading;
+ this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading);
+ // trigger callback
+ if (!isLoading)
+ me.events_loaded($(this).fullCalendar('clientEvents').length);
+ },
+ // callback for date range selection
+ select: function(start, end, allDay, e, view) {
+ var range_select = (!allDay || start.getDate() != end.getDate())
+ if (dialog_check(e) && range_select)
+ event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar });
+ if (range_select || ignore_click)
+ view.calendar.unselect();
+ },
+ // callback for clicks in all-day box
+ dayClick: function(date, allDay, e, view) {
+ var now = new Date().getTime();
+ if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) { // emulate double-click on day
+ var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
+ return event_edit_dialog('new', { start:date, end:enddate, allDay:allDay, calendar:me.selected_calendar });
+ }
+
+ if (!ignore_click) {
+ view.calendar.gotoDate(date);
+ if (day_clicked && new Date(day_clicked).getMonth() != date.getMonth())
+ view.calendar.select(date, date, allDay);
+ }
+ day_clicked = date.getTime();
+ day_clicked_ts = now;
+ },
+ // callback when an event was dragged and finally dropped
+ eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
+ if (event.end == null || event.end.getTime() < event.start.getTime()) {
+ event.end = new Date(event.start.getTime() + (allDay ? DAY_MS : HOUR_MS));
+ }
+ // moved to all-day section: set times to 12:00 - 13:00
+ if (allDay && !event.allDay) {
+ event.start.setHours(12);
+ event.start.setMinutes(0);
+ event.start.setSeconds(0);
+ event.end.setHours(13);
+ event.end.setMinutes(0);
+ event.end.setSeconds(0);
+ }
+ // moved from all-day section: set times to working hours
+ else if (event.allDay && !allDay) {
+ var newstart = event.start.getTime();
+ revertFunc(); // revert to get original duration
+ var numdays = Math.max(1, Math.round((event.end.getTime() - event.start.getTime()) / DAY_MS)) - 1;
+ event.start = new Date(newstart);
+ event.end = new Date(newstart + numdays * DAY_MS);
+ event.end.setHours(settings['work_end'] || 18);
+ event.end.setMinutes(0);
+
+ if (event.end.getTime() < event.start.getTime())
+ event.end = new Date(newstart + HOUR_MS);
+ }
+
+ // send move request to server
+ var data = {
+ id: event.id,
+ calendar: event.calendar,
+ start: date2servertime(event.start),
+ end: date2servertime(event.end),
+ allday: allDay?1:0
+ };
+ update_event_confirm('move', event, data);
+ },
+ // callback for event resizing
+ eventResize: function(event, delta) {
+ // sanitize event dates
+ if (event.allDay)
+ event.start.setHours(12);
+ if (!event.end || event.end.getTime() < event.start.getTime())
+ event.end = new Date(event.start.getTime() + HOUR_MS);
+
+ // send resize request to server
+ var data = {
+ id: event.id,
+ calendar: event.calendar,
+ start: date2servertime(event.start),
+ end: date2servertime(event.end)
+ };
+ update_event_confirm('resize', event, data);
+ },
+ viewDisplay: function(view) {
+ $('#agendaoptions')[view.name == 'table' ? 'show' : 'hide']();
+ if (minical) {
+ window.setTimeout(function(){ minical.datepicker('setDate', fc.fullCalendar('getDate')); }, exec_deferred);
+ if (view.name != current_view)
+ me.view_resize();
+ current_view = view.name;
+ me.update_state();
+ }
+ },
+ viewRender: function(view) {
+ if (fc && view.name == 'month')
+ fc.fullCalendar('option', 'maxHeight', Math.floor((view.element.parent().height()-18) / 6) - 35);
+ }
+ }));
+
+ // format time string
+ var formattime = function(hour, minutes, start) {
+ var time, diff, unit, duration = '', d = new Date();
+ d.setHours(hour);
+ d.setMinutes(minutes);
+ time = $.fullCalendar.formatDate(d, settings['time_format']);
+ if (start) {
+ diff = Math.floor((d.getTime() - start.getTime()) / 60000);
+ if (diff > 0) {
+ unit = 'm';
+ if (diff >= 60) {
+ unit = 'h';
+ diff = Math.round(diff / 3) / 20;
+ }
+ duration = ' (' + diff + unit + ')';
+ }
+ }
+ return [time, duration];
+ };
+
+ var autocomplete_times = function(p, callback) {
+ /* Time completions */
+ var result = [];
+ var now = new Date();
+ var st, start = (String(this.element.attr('id')).indexOf('endtime') > 0
+ && (st = $('#edit-starttime').val())
+ && $('#edit-startdate').val() == $('#edit-enddate').val())
+ ? parse_datetime(st, '') : null;
+ var full = p.term - 1 > 0 || p.term.length > 1;
+ var hours = start ? start.getHours() :
+ (full ? parse_datetime(p.term, '') : now).getHours();
+ var step = 15;
+ var minutes = hours * 60 + (full ? 0 : now.getMinutes());
+ var min = Math.ceil(minutes / step) * step % 60;
+ var hour = Math.floor(Math.ceil(minutes / step) * step / 60);
+ // list hours from 0:00 till now
+ for (var h = start ? start.getHours() : 0; h < hours; h++)
+ result.push(formattime(h, 0, start));
+ // list 15min steps for the next two hours
+ for (; h < hour + 2 && h < 24; h++) {
+ while (min < 60) {
+ result.push(formattime(h, min, start));
+ min += step;
+ }
+ min = 0;
+ }
+ // list the remaining hours till 23:00
+ while (h < 24)
+ result.push(formattime((h++), 0, start));
+
+ return callback(result);
+ };
+
+ var autocomplete_open = function(event, ui) {
+ // scroll to current time
+ var $this = $(this);
+ var widget = $this.autocomplete('widget');
+ var menu = $this.data('ui-autocomplete').menu;
+ var amregex = /^(.+)(a[.m]*)/i;
+ var pmregex = /^(.+)(a[.m]*)/i;
+ var val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
+ var li, html;
+ widget.css('width', '10em');
+ widget.children().each(function(){
+ li = $(this);
+ html = li.children().first().html().replace(/\s+\(.+\)$/, '').replace(amregex, '0:$1').replace(pmregex, '1:$1');
+ if (html.indexOf(val) == 0)
+ menu._scrollIntoView(li);
+ });
+ };
+
+ // if start date is changed, shift end date according to initial duration
+ var shift_enddate = function(dateText) {
+ var newstart = parse_datetime('0', dateText);
+ var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000);
+ $('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format']));
+ event_times_changed();
+ };
+
+ // Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
+ // Uses the default $.datepicker.iso8601Week() function but takes firstDay setting into account.
+ // This is a temporary fix until http://bugs.jqueryui.com/ticket/8420 is resolved.
+ var iso8601Week = datepicker_settings.calculateWeek = function(date) {
+ var mondayOffset = Math.abs(1 - datepicker_settings.firstDay);
+ return $.datepicker.iso8601Week(new Date(date.getTime() + mondayOffset * 86400000));
+ };
+
+ var minical;
+ var init_calendar_ui = function()
+ {
+ // initialize small calendar widget using jQuery UI datepicker
+ minical = $('#datepicker').datepicker($.extend(datepicker_settings, {
+ inline: true,
+ showWeek: true,
+ changeMonth: true,
+ changeYear: true,
+ onSelect: function(dateText, inst) {
+ ignore_click = true;
+ var d = minical.datepicker('getDate'); //parse_datetime('0:0', dateText);
+ fc.fullCalendar('gotoDate', d).fullCalendar('select', d, d, true);
+ },
+ onChangeMonthYear: function(year, month, inst) {
+ minical.data('year', year).data('month', month);
+ },
+ beforeShowDay: function(date) {
+ var view = fc.fullCalendar('getView');
+ var active = view.visStart && date.getTime() >= view.visStart.getTime() && date.getTime() < view.visEnd.getTime();
+ return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), ''];
+ }
+ })) // set event handler for clicks on calendar week cell of the datepicker widget
+ .on('click', 'td.ui-datepicker-week-col', function(e) {
+ var cell = $(e.target);
+ if (e.target.tagName == 'TD') {
+ var base_date = minical.datepicker('getDate');
+ if (minical.data('month'))
+ base_date.setMonth(minical.data('month')-1);
+ if (minical.data('year'))
+ base_date.setYear(minical.data('year'));
+ base_date.setHours(12);
+ base_date.setDate(base_date.getDate() - ((base_date.getDay() + 6) % 7) + datepicker_settings.firstDay);
+ var base_kw = iso8601Week(base_date),
+ target_kw = parseInt(cell.html()),
+ wdiff = target_kw - base_kw;
+ if (wdiff > 10) // year jump
+ base_date.setYear(base_date.getFullYear() - 1);
+ else if (wdiff < -10)
+ base_date.setYear(base_date.getFullYear() + 1);
+ // select monday of the chosen calendar week
+ var day_off = base_date.getDay() - datepicker_settings.firstDay,
+ date = new Date(base_date.getTime() - day_off * DAY_MS + wdiff * 7 * DAY_MS);
+ fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek');
+ minical.datepicker('setDate', date);
+ }
+ });
+
+ minical.find('.ui-datepicker-inline').attr('aria-labelledby', 'aria-label-minical');
+
+ if (rcmail.env.date) {
+ var viewdate = new Date();
+ viewdate.setTime(fromunixtime(rcmail.env.date));
+ minical.datepicker('setDate', viewdate);
+ }
+
+ // init event dialog
+ $('#eventtabs').tabs({
+ activate: function(event, ui) {
+ if (ui.newPanel.selector == '#event-panel-attendees' || ui.newPanel.selector == '#event-panel-resources') {
+ var tab = ui.newPanel.selector == '#event-panel-resources' ? 'resource' : 'attendee';
+ if (!rcube_event.is_keyboard(event))
+ $('#edit-'+tab+'-name').select();
+ // update free-busy status if needed
+ if (freebusy_ui.needsupdate && me.selected_event)
+ update_freebusy_status(me.selected_event);
+ // add current user as organizer if non added yet
+ if (!event_attendees.length) {
+ add_attendee($.extend({ role:'ORGANIZER' }, settings.identity));
+ $('#edit-attendees-form .attendees-invitebox').show();
+ }
+ }
+ // reset autocompletion on tab change (#3389)
+ if (ui.oldPanel.selector == '#event-panel-attendees' || ui.oldPanel.selector == '#event-panel-resources') {
+ rcmail.ksearch_blur();
+ }
+ }
+ });
+ $('#edit-enddate').datepicker(datepicker_settings);
+ $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); });
+ $('#edit-enddate').datepicker('option', 'onSelect', event_times_changed).change(event_times_changed);
+ $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); event_times_changed(); });
+
+ // configure drop-down menu on time input fields based on jquery UI autocomplete
+ $('#edit-starttime, #edit-endtime, #eventedit input.edit-alarm-time')
+ .attr('autocomplete', "off")
+ .autocomplete({
+ delay: 100,
+ minLength: 1,
+ appendTo: '#eventedit',
+ source: autocomplete_times,
+ open: autocomplete_open,
+ change: event_times_changed,
+ select: function(event, ui) {
+ $(this).val(ui.item[0]).change();
+ return false;
+ }
+ })
+ .click(function() { // show drop-down upon clicks
+ $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
+ }).each(function(){
+ $(this).data('ui-autocomplete')._renderItem = function(ul, item) {
+ return $('<li>')
+ .data('ui-autocomplete-item', item)
+ .append('<a>' + item[0] + item[1] + '</a>')
+ .appendTo(ul);
+ };
+ });
+
+ // adjust end time when changing start
+ $('#edit-starttime').change(function(e) {
+ var dstart = $('#edit-startdate'),
+ newstart = parse_datetime(this.value, dstart.val()),
+ newend = new Date(newstart.getTime() + dstart.data('duration') * 1000);
+ $('#edit-endtime').val($.fullCalendar.formatDate(newend, me.settings['time_format']));
+ $('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format']));
+ event_times_changed();
+ });
+
+ // register events on alarms and recurrence fields
+ me.init_alarms_edit('#edit-alarms');
+ me.init_recurrence_edit('#eventedit');
+
+ // reload free-busy status when changing the organizer identity
+ $('#eventedit').on('change', '#edit-identities-list', function(e) {
+ var email = settings.identities[$(this).val()],
+ icon = $(this).closest('tr').find('img.availabilityicon');
+
+ if (email && icon.length) {
+ icon.attr('data-email', email);
+ check_freebusy_status(icon, email, me.selected_event);
+ }
+ });
+
+ $('#event-export-startdate').datepicker(datepicker_settings);
+
+ // init attendees autocompletion
+ var ac_props;
+ // parallel autocompletion
+ if (rcmail.env.autocomplete_threads > 0) {
+ ac_props = {
+ threads: rcmail.env.autocomplete_threads,
+ sources: rcmail.env.autocomplete_sources
+ };
+ }
+ rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
+ rcmail.addEventListener('autocomplete_insert', function(e) {
+ var success = false;
+ if (e.field.name == 'participant') {
+ success = add_attendees(e.insert, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:(e.data && e.data.type == 'group' ? 'GROUP' : 'INDIVIDUAL') });
+ }
+ else if (e.field.name == 'resource' && e.data && e.data.email) {
+ success = add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
+ }
+ if (e.field && success) {
+ e.field.value = '';
+ }
+ });
+
+ $('#edit-attendee-add').click(function(){
+ var input = $('#edit-attendee-name');
+ rcmail.ksearch_blur();
+ if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
+ input.val('');
+ }
+ });
+
+ rcmail.init_address_input_events($('#edit-resource-name'), { action:'calendar/resources-autocomplete' });
+
+ $('#edit-resource-add').click(function(){
+ var input = $('#edit-resource-name');
+ rcmail.ksearch_blur();
+ if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })) {
+ input.val('');
+ }
+ });
+
+ $('#edit-resource-find').click(function(){
+ event_resources_dialog();
+ return false;
+ });
+
+ // handle change of "send invitations" checkbox
+ $('#edit-attendees-invite').change(function() {
+ $('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked);
+ // hide/show comment field
+ $('#eventedit .attendees-commentbox')[this.checked ? 'show' : 'hide']();
+ });
+
+ // delegate change event to "send invitations" checkbox
+ $('#edit-attendees-donotify').change(function() {
+ $('#edit-attendees-invite').click();
+ return false;
+ });
+
+ $('#edit-attendee-schedule').click(function(){
+ event_freebusy_dialog();
+ });
+
+ $('#shedule-freebusy-prev').html(bw.ie6 ? '&lt;&lt;' : '&#9668;').button().click(function(){ render_freebusy_grid(-1); });
+ $('#shedule-freebusy-next').html(bw.ie6 ? '&gt;&gt;' : '&#9658;').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset();
+
+ $('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); });
+ $('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); });
+
+ $('#schedule-freebusy-workinghours').click(function(){
+ freebusy_ui.workinhoursonly = this.checked;
+ $('#workinghourscss').remove();
+ if (this.checked)
+ $('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head');
+ });
+
+ $('#event-rsvp input.button').click(function(e) {
+ event_rsvp(this)
+ });
+
+ $('#eventedit input.edit-recurring-savemode').change(function(e) {
+ var sel = $('input.edit-recurring-savemode:checked').val(),
+ disabled = sel == 'current' || sel == 'future';
+ $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', disabled);
+ $('#event-panel-recurrence, #event-panel-attachments')[(disabled?'addClass':'removeClass')]('disabled');
+ })
+
+ $('#eventshow .changersvp').click(function(e) {
+ var d = $('#eventshow'),
+ h = -$(this).closest('.event-line').toggle().height();
+ $('#event-rsvp').slideDown(300, function() {
+ h += $(this).height();
+ me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
+ });
+ return false;
+ })
+
+ // register click handler for message links
+ $('#edit-event-links, #event-links').on('click', 'li a.messagelink', function(e) {
+ rcmail.open_window(this.href);
+ if (!rcube_event.is_keyboard(e) && this.blur)
+ this.blur();
+ return false;
+ });
+
+ // register click handler for message delete buttons
+ $('#edit-event-links').on('click', 'li a.delete', function(e) {
+ remove_link(e.target);
+ return false;
+ });
+
+ $('#agenda-listrange').change(function(e){
+ settings['agenda_range'] = parseInt($(this).val());
+ fc.fullCalendar('option', 'listRange', settings['agenda_range']).fullCalendar('render');
+ // TODO: save new settings in prefs
+ }).val(settings['agenda_range']);
+
+ $('#agenda-listsections').change(function(e){
+ settings['agenda_sections'] = $(this).val();
+ fc.fullCalendar('option', 'listSections', settings['agenda_sections']).fullCalendar('render');
+ // TODO: save new settings in prefs
+ }).val(fc.fullCalendar('option', 'listSections'));
+
+ // hide event dialog when clicking somewhere into document
+ $(document).bind('mousedown', dialog_check);
+
+ rcmail.set_busy(false, 'loading', ui_loading);
+ }
+
+ // initialize more UI elements (deferred)
+ window.setTimeout(init_calendar_ui, exec_deferred);
+
+ // fetch counts for some calendars
+ fetch_counts();
+
+ // add proprietary css styles if not IE
+ if (!bw.ie)
+ $('div.fc-content').addClass('rcube-fc-content');
+
+ // IE supresses 2nd click event when double-clicking
+ if (bw.ie && bw.vendver < 9) {
+ $('div.fc-content').bind('dblclick', function(e){
+ if (!$(this).hasClass('fc-widget-header') && fc.fullCalendar('getView').name != 'table') {
+ var date = fc.fullCalendar('getDate');
+ var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
+ event_edit_dialog('new', { start:date, end:enddate, allDay:true, calendar:me.selected_calendar });
+ }
+ });
+ }
+} // end rcube_calendar class
+
+
+/* calendar plugin initialization */
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+ // configure toolbar buttons
+ rcmail.register_command('addevent', function(){ cal.add_event(); }, true);
+ rcmail.register_command('print', function(){ cal.print_calendars(); }, true);
+
+ // configure list operations
+ rcmail.register_command('calendar-create', function(props){ cal.calendar_edit_dialog($.extend($.parseJSON(props), { name:'', color:'cc0000', editable:true, showalarms:true })); }, true);
+ rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('calendar-delete', function(){ cal.calendar_delete(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, true);
+ rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('event-download', function(){ cal.event_download(cal.selected_event); }, true);
+ rcmail.register_command('event-sendbymail', function(p, obj, e){ cal.event_sendbymail(cal.selected_event, e); }, true);
+ rcmail.register_command('event-history', function(p, obj, e){ cal.event_history_dialog(cal.selected_event); }, false);
+
+ // search and export events
+ rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true);
+ rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
+ rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
+
+ // resource invitation dialog
+ rcmail.register_command('search-resource', function(){ cal.resource_search(); }, true);
+ rcmail.register_command('reset-resource-search', function(){ cal.reset_resource_search(); }, true);
+ rcmail.register_command('add-resource', function(){ cal.add_resource2event(); }, false);
+
+ // register callback commands
+ rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); });
+ rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); });
+ rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
+ rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
+ rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); });
+ rcmail.addEventListener('plugin.update_counts', function(p){ cal.update_counts(p); });
+ rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); });
+ rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); });
+ rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); });
+ rcmail.addEventListener('plugin.render_event_changelog', function(data){ cal.render_event_changelog(data); });
+ rcmail.addEventListener('plugin.event_show_diff', function(data){ cal.event_show_diff(data); });
+ rcmail.addEventListener('plugin.close_history_dialog', function(data){ cal.close_history_dialog(); });
+ rcmail.addEventListener('plugin.event_show_revision', function(data){ cal.event_show_dialog(data, null, true); });
+ rcmail.addEventListener('plugin.itip_message_processed', function(data){ cal.itip_message_processed(data); });
+ rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
+
+ // let's go
+ var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
+
+ $(window).resize(function(e) {
+ // check target due to bugs in jquery
+ // http://bugs.jqueryui.com/ticket/7514
+ // http://bugs.jquery.com/ticket/9841
+ if (e.target == window) {
+ cal.view_resize();
+ }
+ }).resize();
+
+ // show calendars list when ready
+ $('#calendars').css('visibility', 'inherit');
+
+ // show toolbar
+ $('#toolbar').show();
+
+});
diff --git a/calendar/composer.json b/calendar/composer.json
new file mode 100644
index 0000000..e0c9b6d
--- /dev/null
+++ b/calendar/composer.json
@@ -0,0 +1,31 @@
+{
+ "name": "kolab/calendar",
+ "type": "roundcube-plugin",
+ "description": "Calendar plugin",
+ "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
+ "license": "AGPLv3",
+ "version": "3.2.8",
+ "authors": [
+ {
+ "name": "Thomas Bruederli",
+ "email": "bruederli@kolabsys.com",
+ "role": "Lead"
+ },
+ {
+ "name": "Alensader Machniak",
+ "email": "machniak@kolabsys.com",
+ "role": "Developer"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "http://plugins.roundcube.net"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "roundcube/plugin-installer": ">=0.1.3",
+ "kolab/libcalendaring": ">=3.2.8"
+ }
+}
diff --git a/calendar/config.inc.php.dist b/calendar/config.inc.php.dist
new file mode 100644
index 0000000..7d771c3
--- /dev/null
+++ b/calendar/config.inc.php.dist
@@ -0,0 +1,198 @@
+<?php
+/*
+ +-------------------------------------------------------------------------+
+ | Configuration for the Calendar plugin |
+ | |
+ | Copyright (C) 2010, Lazlo Westerhof - Netherlands |
+ | Copyright (C) 2011-2014, 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 <http://www.gnu.org/licenses/>. |
+ | |
+ +-------------------------------------------------------------------------+
+ | Author: Lazlo Westerhof <hello@lazlo.me> |
+ | Thomas Bruederli <bruederli@kolabsys.com> |
+ +-------------------------------------------------------------------------+
+*/
+
+// backend type (database, kolab, caldav, ical)
+$config['calendar_driver'] = array("kolab");
+$config['calendar_driver_default'] = "kolab";
+
+// default calendar view (agendaDay, agendaWeek, month)
+$config['calendar_default_view'] = "agendaWeek";
+
+// show a birthdays calendar from the user's address book(s)
+$config['calendar_contact_birthdays'] = false;
+
+// mapping of Roundcube date formats to calendar formats (long/short/agenda)
+// should be in sync with 'date_formats' in main config
+$config['calendar_date_format_sets'] = array(
+ 'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
+ 'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
+ 'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
+ 'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
+ 'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
+ 'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
+ 'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
+);
+
+// general date format (only set if different from default date format and not user configurable)
+// $config['calendar_date_format'] = "yyyy-MM-dd";
+
+// time format (only set if different from default date format)
+// $config['calendar_time_format'] = "HH:mm";
+
+// short date format (used for column titles)
+// $config['calendar_date_short'] = 'M-d';
+
+// long date format (used for calendar title)
+// $config['calendar_date_long'] = 'MMM d yyyy';
+
+// date format used for agenda view
+// $config['calendar_date_agenda'] = 'ddd MM-dd';
+
+// timeslots per hour (1, 2, 3, 4, 6)
+$config['calendar_timeslots'] = 2;
+
+// show this number of days in agenda view
+$config['calendar_agenda_range'] = 60;
+
+// first day of the week (0-6)
+$config['calendar_first_day'] = 1;
+
+// first hour of the calendar (0-23)
+$config['calendar_first_hour'] = 6;
+
+// working hours begin
+$config['calendar_work_start'] = 6;
+
+// working hours end
+$config['calendar_work_end'] = 18;
+
+// show line at current time of the day
+$config['calendar_time_indicator'] = true;
+
+// default alarm settings for new events.
+// this is only a preset when a new event dialog opens
+// possible values are <empty>, DISPLAY, EMAIL
+$config['calendar_default_alarm_type'] = '';
+
+// default alarm offset for new events.
+// use ical-style offset values like "-1H" (one hour before) or "+30M" (30 minutes after)
+$config['calendar_default_alarm_offset'] = '-15M';
+
+// how to colorize events:
+// 0: according to calendar color
+// 1: according to category color
+// 2: calendar for outer, category for inner color
+// 3: category for outer, calendar for inner color
+$config['calendar_event_coloring'] = 0;
+
+// event categories
+$config['calendar_categories'] = array(
+ 'Personal' => 'c0c0c0',
+ 'Work' => 'ff0000',
+ 'Family' => '00ff00',
+ 'Holiday' => 'ff6600',
+);
+
+// enable users to invite/edit attendees for shared events organized by others
+$config['calendar_allow_invite_shared'] = false;
+
+// allow users to accecpt iTip invitations who are no explicitly listed as attendee.
+// this can be the case if invitations are sent to mailing lists or alias email addresses.
+$config['calendar_allow_itip_uninvited'] = true;
+
+// controls the visibility/default of the checkbox controlling the sending of iTip invitations
+// 0 = hidden + disabled
+// 1 = hidden + active
+// 2 = visible + unchecked
+// 3 = visible + active
+$config['calendar_itip_send_option'] = 3;
+
+// Action taken after iTip request is handled. Possible values:
+// 0 - no action
+// 1 - move to Trash
+// 2 - delete the message
+// 3 - flag as deleted
+// folder_name - move the message to the specified folder
+$config['calendar_itip_after_action'] = 0;
+
+// enable asynchronous free-busy triggering after data changed
+$config['calendar_freebusy_trigger'] = false;
+
+// free-busy information will be displayed for user calendars if available
+// 0 - no free-busy information
+// 1 - enabled in all views
+// 2 - only in quickview
+$config['calendar_include_freebusy_data'] = 1;
+
+// SMTP server host used to send (anonymous) itip messages.
+// Set to '' in order to use PHP's mail() function for email delivery.
+// To override the SMTP port or connection method, provide a full URL like 'tls://somehost:587'
+$config['calendar_itip_smtp_server'] = null;
+
+// SMTP username used to send (anonymous) itip messages
+$config['calendar_itip_smtp_user'] = 'smtpauth';
+
+// SMTP password used to send (anonymous) itip messages
+$config['calendar_itip_smtp_pass'] = '123456';
+
+// Base URL to build fully qualified URIs to access calendars via CALDAV
+// The following replacement variables are supported:
+// %h - Current HTTP host
+// %u - Current webmail user name
+// %n - Calendar name
+// %i - Calendar UUID
+// $config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i';
+
+// Crypt key to encrypt passwords for added iCAL/CalDAV calendars
+$config['calendar_crypt_key'] = "put some random string here";
+
+// 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'] = true;
+
+// Driver to provide a resource directory ('ldap' is the only implementation yet).
+// Leave empty or commented to disable resources support.
+// $config['calendar_resources_driver'] = 'ldap';
+
+// LDAP directory configuration to find avilable resources for events
+// $config['calendar_resources_directory'] = array(/* ldap_public-like address book configuration */);
+
+// Enable debugging output for iCAL/CalDAV drivers
+$config['calendar_caldav_debug'] = false;
+$config['calendar_ical_debug'] = false;
+
+// Pre-installed calendars, added at first access to calendar section
+// Caldav driver is supported only
+// $config['calendar_preinstalled_calendars'] = array(
+// 'Caldav' => array(
+// 'driver' => 'caldav',
+// 'caldav_user' => '%u',
+// 'caldav_pass' => '%p',
+// 'caldav_url' => 'http://example.caldav.org/%u/calendar/',
+// 'color' => 'cccc00',
+// 'showAlarms' => 1),
+// 'Other' => array(
+// 'driver' => 'other',
+// 'other_user' => 'user@example.other.org',
+// 'other_pass' => 'password',
+// 'other_url' => 'http://example.other.org/user@example.other.org/other',
+// 'color' => 'cc0000',
+// 'other_property1' => 'value1',
+// 'other_property2' => 'value2',
+// 'showAlarms' => 1));
+?>
diff --git a/calendar/drivers/caldav/SQL/mysql.initial.sql b/calendar/drivers/caldav/SQL/mysql.initial.sql
new file mode 100644
index 0000000..d60d482
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql.initial.sql
@@ -0,0 +1,92 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+CREATE TABLE IF NOT EXISTS `caldav_calendars` (
+ `calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(1) NOT NULL DEFAULT '1',
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_user` varchar(255) DEFAULT NULL,
+ `caldav_pass` varchar(1024) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`calendar_id`),
+ INDEX `caldav_user_name_idx` (`user_id`, `name`),
+ CONSTRAINT `fk_caldav_calendars_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_events` (
+ `event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `uid` varchar(255) NOT NULL DEFAULT '',
+ `instance` varchar(16) NOT NULL DEFAULT '',
+ `isexception` tinyint(1) NOT NULL DEFAULT '0',
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
+ `start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `recurrence` varchar(255) DEFAULT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text NOT NULL,
+ `location` varchar(255) NOT NULL DEFAULT '',
+ `categories` varchar(255) NOT NULL DEFAULT '',
+ `url` varchar(255) NOT NULL DEFAULT '',
+ `all_day` tinyint(1) NOT NULL DEFAULT '0',
+ `free_busy` tinyint(1) NOT NULL DEFAULT '0',
+ `priority` tinyint(1) NOT NULL DEFAULT '0',
+ `sensitivity` tinyint(1) NOT NULL DEFAULT '0',
+ `status` varchar(32) NOT NULL DEFAULT '',
+ `alarms` text NULL DEFAULT NULL,
+ `attendees` text DEFAULT NULL,
+ `notifyat` datetime DEFAULT NULL,
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`event_id`),
+ INDEX `caldav_uid_idx` (`uid`),
+ INDEX `caldav_recurrence_idx` (`recurrence_id`),
+ INDEX `caldav_calendar_notify_idx` (`calendar_id`,`notifyat`),
+ CONSTRAINT `fk_caldav_events_calendar_id` FOREIGN KEY (`calendar_id`)
+ REFERENCES `caldav_calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_attachments` (
+ `attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `filename` varchar(255) NOT NULL DEFAULT '',
+ `mimetype` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ PRIMARY KEY(`attachment_id`),
+ CONSTRAINT `fk_caldav_attachments_event_id` FOREIGN KEY (`event_id`)
+ REFERENCES `caldav_events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2015022700'); \ No newline at end of file
diff --git a/calendar/drivers/caldav/SQL/mysql/.keep_dir b/calendar/drivers/caldav/SQL/mysql/.keep_dir
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/.keep_dir
diff --git a/calendar/drivers/caldav/SQL/mysql/2014081300.sql b/calendar/drivers/caldav/SQL/mysql/2014081300.sql
new file mode 100644
index 0000000..f1a3c98
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/2014081300.sql
@@ -0,0 +1,24 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+ALTER TABLE `caldav_props` change `user` `username` varchar(255);
+ALTER TABLE `events` ADD `status` VARCHAR(32) NOT NULL DEFAULT '' AFTER `sensitivity`; \ No newline at end of file
diff --git a/calendar/drivers/caldav/SQL/mysql/2015022500.sql b/calendar/drivers/caldav/SQL/mysql/2015022500.sql
new file mode 100644
index 0000000..df0f613
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/2015022500.sql
@@ -0,0 +1,125 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Create new tables */
+CREATE TABLE IF NOT EXISTS `caldav_calendars` (
+ `calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(1) NOT NULL DEFAULT '1',
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_user` varchar(255) DEFAULT NULL,
+ `caldav_pass` varchar(1024) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`calendar_id`),
+ INDEX `caldav_user_name_idx` (`user_id`, `name`),
+ CONSTRAINT `fk_caldav_calendars_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_events` (
+ `event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `uid` varchar(255) NOT NULL DEFAULT '',
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
+ `start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `recurrence` varchar(255) DEFAULT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text NOT NULL,
+ `location` varchar(255) NOT NULL DEFAULT '',
+ `categories` varchar(255) NOT NULL DEFAULT '',
+ `url` varchar(255) NOT NULL DEFAULT '',
+ `all_day` tinyint(1) NOT NULL DEFAULT '0',
+ `free_busy` tinyint(1) NOT NULL DEFAULT '0',
+ `priority` tinyint(1) NOT NULL DEFAULT '0',
+ `sensitivity` tinyint(1) NOT NULL DEFAULT '0',
+ `status` varchar(32) NOT NULL DEFAULT '',
+ `alarms` varchar(255) DEFAULT NULL,
+ `attendees` text DEFAULT NULL,
+ `notifyat` datetime DEFAULT NULL,
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`event_id`),
+ INDEX `caldav_uid_idx` (`uid`),
+ INDEX `caldav_recurrence_idx` (`recurrence_id`),
+ INDEX `caldav_calendar_notify_idx` (`calendar_id`,`notifyat`),
+ CONSTRAINT `fk_caldav_events_calendar_id` FOREIGN KEY (`calendar_id`)
+ REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_attachments` (
+ `attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `filename` varchar(255) NOT NULL DEFAULT '',
+ `mimetype` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ PRIMARY KEY(`attachment_id`),
+ CONSTRAINT `fk_caldav_attachments_event_id` FOREIGN KEY (`event_id`)
+ REFERENCES `events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+/* Migrate Data */
+INSERT INTO caldav_calendars SELECT calendar_id, user_id, `name`, color, showalarms, url as caldav_url,
+ tag as caldav_tag, username as caldav_user, pass as caldav_pass,
+ last_change as caldav_last_change
+FROM calendars cal, caldav_props dav
+WHERE dav.obj_id = cal.calendar_id
+AND dav.obj_type = 'vcal';
+
+INSERT INTO caldav_events SELECT e.*, dav.url as caldav_url, dav.tag as caldav_tag, dav.last_change as caldav_last_change
+FROM `events` e, caldav_props dav
+WHERE dav.obj_id = e.event_id
+AND dav.obj_type = 'vevent';
+
+INSERT INTO caldav_attachments SELECT * FROM attachments a
+WHERE a.event_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+
+/* Drop deprecated data */
+DELETE FROM `events` WHERE event_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+DELETE FROM calendars WHERE calendar_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vcal'
+);
+DELETE FROM attachments WHERE event_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+DROP TABLE caldav_props;
+
diff --git a/calendar/drivers/caldav/SQL/mysql/2015022700.sql b/calendar/drivers/caldav/SQL/mysql/2015022700.sql
new file mode 100644
index 0000000..f44b49e
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/2015022700.sql
@@ -0,0 +1,14 @@
+-- add identifier for recurring instances and exceptions
+
+ALTER TABLE `caldav_events` ADD `instance` varchar(16) NOT NULL DEFAULT '' AFTER `uid`;
+ALTER TABLE `caldav_events` ADD `isexception` tinyint(1) NOT NULL DEFAULT '0' AFTER `instance`;
+
+UPDATE `caldav_events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%d')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 1;
+
+UPDATE `caldav_events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%dT%k%i%s')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 0;
+
+-- extend alarms columns for multiple values
+
+ALTER TABLE `caldav_events` CHANGE `alarms` `alarms` TEXT NULL DEFAULT NULL; \ No newline at end of file
diff --git a/calendar/drivers/caldav/SQL/postgres.initial.sql b/calendar/drivers/caldav/SQL/postgres.initial.sql
new file mode 100644
index 0000000..f49da4e
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/postgres.initial.sql
@@ -0,0 +1,51 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Hugo Slabbert <hugo@slabnet.com>
+ *
+ * Copyright (C) 2014, Hugo Slabbert <hugo@slabnet.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+CREATE TYPE caldav_type AS ENUM ('vcal','vevent','vtodo','');
+
+CREATE TABLE IF NOT EXISTS caldav_props (
+ obj_id int NOT NULL,
+ obj_type caldav_type NOT NULL,
+ url varchar(255) NOT NULL,
+ tag varchar(255) DEFAULT NULL,
+ username varchar(255) DEFAULT NULL,
+ pass varchar(1024) DEFAULT NULL,
+ last_change timestamp without time zone DEFAULT now() NOT NULL,
+ PRIMARY KEY (obj_id, obj_type)
+);
+
+CREATE OR REPLACE FUNCTION upd_timestamp() RETURNS TRIGGER
+LANGUAGE plpgsql
+AS
+$$
+BEGIN
+ NEW.last_change = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$;
+
+CREATE TRIGGER update_timestamp
+ BEFORE INSERT OR UPDATE
+ ON caldav_props
+ FOR EACH ROW
+ EXECUTE PROCEDURE upd_timestamp();
+
diff --git a/calendar/drivers/caldav/caldav_driver.php b/calendar/drivers/caldav/caldav_driver.php
new file mode 100644
index 0000000..b39aeff
--- /dev/null
+++ b/calendar/drivers/caldav/caldav_driver.php
@@ -0,0 +1,2036 @@
+<?php
+
+/**
+ * CalDAV driver for the Calendar plugin
+ *
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once (dirname(__FILE__).'/caldav_sync.php');
+require_once (dirname(__FILE__).'/../../lib/encryption.php');
+
+
+class caldav_driver extends calendar_driver
+{
+ const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
+ public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'cancelled');
+
+ // features this backend supports
+ public $alarms = true;
+ public $attendees = true;
+ public $freebusy = false;
+ public $attachments = true;
+ public $alarm_types = array('DISPLAY');
+
+ private $rc;
+ private $cal;
+ private $cache = array();
+ private $calendars = array();
+ private $calendar_ids = '';
+ private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3);
+ private $sensitivity_map = array('public' => 0, 'private' => 1, 'confidential' => 2);
+ private $server_timezone;
+
+ private $db_events = 'caldav_events';
+ private $db_calendars = 'caldav_calendars';
+ private $db_attachments = 'caldav_attachments';
+
+ // Crypt key for CalDAV auth
+ private $crypt_key;
+
+ // Holds CalDAV sync clients
+ private $sync_clients = array();
+
+ // Min. time period to wait until CalDAV sync check.
+ private $sync_period = 10; // seconds
+
+ // Indicates debug mode for CalDAV
+ static private $debug = null;
+
+ /**
+ * Helper method to log debug msg if debug mode is enabled.
+ */
+ static public function debug_log($msg)
+ {
+ if(self::$debug === true)
+ rcmail::console(__CLASS__.': '.$msg);
+ }
+
+ /**
+ * Helper method to log (if debug mode is enabled) and raise an user error.
+ */
+ private function _raise_error($msg)
+ {
+ self::debug_log($msg);
+ $this->rc->output->show_message($msg, 'error');
+ }
+
+ /**
+ * Default constructor
+ */
+ public function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->server_timezone = new DateTimeZone(date_default_timezone_get());
+
+ // read database config
+ $db = $this->rc->get_dbh();
+ $this->db_events = $this->rc->config->get('db_table_caldav_events', $db->table_name($this->db_events));
+ $this->db_calendars = $this->rc->config->get('db_table_caldav_calendars', $db->table_name($this->db_calendars));
+ $this->db_attachments = $this->rc->config->get('db_table_caldav_attachments', $db->table_name($this->db_attachments));
+ $this->crypt_key = $this->rc->config->get("calendar_crypt_key", "%E`c{2;<J2F^4_&._BxfQ<5Pf3qv!m{e");
+
+ // Set debug state
+ if(self::$debug === null)
+ self::$debug = $this->rc->config->get('calendar_caldav_debug', False);
+
+ $this->_read_calendars();
+ }
+
+ /**
+ * Read available calendars for the current user and store them internally
+ */
+ protected function _read_calendars()
+ {
+ $hidden = array_filter(explode(',', $this->rc->config->get('hidden_caldav_calendars', '')));
+
+ if (!empty($this->rc->user->ID)) {
+ $calendar_ids = array();
+ $result = $this->rc->db->query("SELECT *, calendar_id AS id
+ FROM " . $this->db_calendars . "
+ WHERE user_id=?
+ ORDER BY name",
+ $this->rc->user->ID
+ );
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $arr['showalarms'] = intval($arr['showalarms']);
+ $arr['active'] = !in_array($arr['id'], $hidden);
+ $arr['name'] = html::quote($arr['name']);
+ $arr['listname'] = html::quote($arr['name']);
+ $arr['rights'] = 'lrswikxteav';
+ $arr['editable'] = true;
+ $arr['caldav_pass'] = $this->_decrypt_pass($arr['caldav_pass']);
+ $this->calendars[$arr['calendar_id']] = $arr;
+ $calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
+
+ // Init sync client
+ $cal_id = $arr['calendar_id'];
+ $this->sync_clients[$cal_id] = new caldav_sync($arr);
+ }
+ $this->calendar_ids = join(',', $calendar_ids);
+ }
+ }
+
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param integer Bitmask defining filter criterias
+ *
+ * @return array List of calendars
+ */
+ public function list_calendars($filter = 0)
+ {
+ $calendars = $this->calendars;
+
+ // filter active calendars
+ if ($filter & self::FILTER_ACTIVE) {
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ }
+
+ // 'personal' is unsupported in this driver
+
+ return $calendars;
+ }
+
+ /**
+ * Extracts CalDAV calendar.
+ *
+ * @see database_driver::create_calendar()
+ */
+ public function create_calendar($cal)
+ {
+ $result = false;
+ $cal['caldav_url'] = self::_encode_url($cal["caldav_url"]);
+ if(!isset($cal['color'])) $cal['color'] = 'cc0000';
+
+ $calendars = $this->_autodiscover_calendars($this->_expand_pass($cal));
+ $cal_ids = array();
+
+ if($calendars)
+ {
+ $result = true;
+ foreach ($calendars as $calendar)
+ {
+ // Skip already existent calendars
+ $result = $this->rc->db->query("SELECT * FROM ".$this->db_calendars." WHERE user_id=? and caldav_url LIKE ?", $this->rc->user->ID, $calendar['href']);
+ if($this->rc->db->affected_rows($result)) continue;
+
+ $cal['caldav_url'] = self::_encode_url($calendar['href']);
+
+ // Respect $props['name'] if only a single calendar was found e.g. no auto-discovery.
+ if(sizeof($calendars) > 1 || !isset($cal['name']) || $cal['name'] == "")
+ $cal['name'] = $calendar['name'];
+
+ if (($obj_id = $this->_db_create_calendar($cal)) !== false) {
+ array_push($cal_ids, $obj_id);
+ } else $result = false;
+ }
+ }
+
+ // Sync newly created calendars
+ if($cal_ids) {
+
+ // Re-read calendars to internal buffer.
+ $this->_read_calendars();
+
+ // Initial sync of newly created calendars.
+ foreach ($cal_ids as $cal_id) {
+ $this->_sync_calendar($cal_id);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * caldav_url: CalDAV calendar URL
+ * caldav_tag: CalDAV calendar ctag
+ * caldav_user: CalDAV authentication user
+ * caldav_pass: CalDAV authentication password
+ *
+ * @return mixed ID of the calendar on success, False on error
+ */
+ private function _db_create_calendar($prop)
+ {
+ $result = $this->rc->db->query(
+ "INSERT INTO " . $this->db_calendars . "
+ (user_id, name, color, showalarms, caldav_url, caldav_tag, caldav_user, caldav_pass)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ $this->rc->user->ID,
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms']?1:0,
+ $prop['caldav_url'],
+ isset($prop["caldav_tag"]) ? $prop["caldav_tag"] : null,
+ isset($prop["caldav_user"]) ? $prop["caldav_user"] : null,
+ isset($prop["caldav_pass"]) ? $this->_encrypt_pass($prop["caldav_pass"]) : null
+ );
+
+ if ($result)
+ return $this->rc->db->insert_id($this->db_calendars);
+
+ return false;
+ }
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function edit_calendar($cal)
+ {
+ $query = $this->rc->db->query("UPDATE " . $this->db_calendars . "
+ SET name=?, color=?, showalarms=?, caldav_url=?, caldav_tag=?, caldav_user=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $cal['name'],
+ $cal['color'],
+ $cal['showalarms']?1:0,
+ $cal['caldav_url'],
+ isset($cal["caldav_tag"]) ? $cal["caldav_tag"] : null,
+ isset($cal["caldav_user"]) ? $cal["caldav_user"] : null,
+ $cal['id'],
+ $this->rc->user->ID
+ );
+
+ // Change password if specified
+ if (isset($cal["caldav_pass"])) {
+ $query = $this->rc->db->query("UPDATE " . $this->db_calendars . "
+ SET caldav_pass=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $this->_encrypt_pass($cal['caldav_pass']),
+ $cal['id'],
+ $this->rc->user->ID
+ );
+ }
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Set active/subscribed state of a calendar
+ * Save a list of hidden calendars in user prefs
+ *
+ * @see calendar_driver::subscribe_calendar()
+ */
+ public function subscribe_calendar($prop)
+ {
+ $hidden = array_flip(explode(',', $this->rc->config->get('hidden_caldav_calendars', '')));
+
+ if ($prop['active'])
+ unset($hidden[$prop['id']]);
+ else
+ $hidden[$prop['id']] = 1;
+
+ return $this->rc->user->save_prefs(array('hidden_caldav_calendars' => join(',', array_keys($hidden))));
+ }
+
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @see calendar_driver::delete_calendar()
+ */
+ public function delete_calendar($prop)
+ {
+ if (!$this->calendars[$prop['id']])
+ return false;
+
+ // events and attachments will be deleted by foreign key cascade
+
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_calendars . " WHERE calendar_id=?",
+ $prop['id']
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Add a single event to the database
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::new_event()
+ */
+ public function new_event($event)
+ {
+ if (!$this->validate($event))
+ return false;
+
+ if (!empty($this->calendars)) {
+ if ($event['calendar'] && !$this->calendars[$event['calendar']])
+ return false;
+ if (!$event['calendar'])
+ $event['calendar'] = reset(array_keys($this->calendars));
+
+ if($event = $this->_save_preprocess($event)) {
+
+ $sync_client = $this->sync_clients[$event["calendar"]];
+
+ // Only push event if caldav_tag is not set to avoid pushing it twice
+ if (isset($event["caldav_tag"]) || ($event = $sync_client->create_event($event)) !== false) {
+
+ if ($event_id = $this->_insert_event($event)) {
+ $this->_update_recurring($event);
+ }
+ }
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ *
+ */
+ private function _insert_event(&$event)
+ {
+ //$event = $this->_save_preprocess($event);
+
+ $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, created, changed, uid, recurrence_id, instance, isexception, %s, %s, all_day, recurrence,
+ title, description, location, categories, url, free_busy, priority, sensitivity, status, attendees, alarms, notifyat,
+ caldav_url, caldav_tag)
+ VALUES (?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['calendar'],
+ strval($event['uid']),
+ intval($event['recurrence_id']),
+ strval($event['_instance']),
+ intval($event['isexception']),
+ $event['start']->format(self::DB_DATE_FORMAT),
+ $event['end']->format(self::DB_DATE_FORMAT),
+ intval($event['all_day']),
+ $event['_recurrence'],
+ strval($event['title']),
+ strval($event['description']),
+ strval($event['location']),
+ join(',', (array)$event['categories']),
+ strval($event['url']),
+ intval($event['free_busy']),
+ intval($event['priority']),
+ intval($event['sensitivity']),
+ strval($event['status']),
+ $event['attendees'],
+ $event['alarms'],
+ $event['notifyat'],
+ $event['caldav_url'],
+ $event['caldav_tag']
+ );
+
+ $event_id = $this->rc->db->insert_id($this->db_events);
+
+ if ($event_id) {
+ $event['id'] = $event_id;
+
+ // add attachments
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event_id);
+ unset($attachment);
+ }
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update the event entry with the given data and sync with caldav server.
+ *
+ * @param array Hash array with event properties
+ * @param array Internal use only, filled with non-modified event if this is second try after a calendar sync was enforced first.
+ * @see caldav_driver::_db_edit_event()
+ * @return bool
+ */
+ public function edit_event($event, $old_event = null)
+ {
+ $sync_enforced = ($old_event != null);
+ $event_id = (int)$event["id"];
+ $cal_id = $event["calendar"];
+
+ if($old_event == null)
+ $old_event = $this->get_event($event);
+
+ if($this->_db_edit_event($event))
+ {
+ // Re-load updated event and push to caldav.
+ $event = $this->get_event(array("id" => $event_id));
+
+ $sync_client = $this->sync_clients[$cal_id];
+ $success = $sync_client->update_event($event);
+
+ if($success === true)
+ {
+ self::debug_log("Successfully updated event \"$event_id\".");
+
+ // Trigger calendar sync to update ctags and etags.
+ $this->_sync_calendar($cal_id);
+
+ return true;
+ }
+ else if($success < 0 && $sync_enforced == false)
+ {
+ self::debug_log("Event \"$event_id\", tag \"".$event["caldav_tag"]."\" not up to date, will update calendar first ...");
+ $this->_sync_calendar($cal_id);
+
+ return $this->edit_event($event, $old_event); // Re-try after re-sync
+ }
+ else
+ {
+ $this->_db_edit_event($old_event);
+ $this->_raise_error("Could not update event: Unexpected CalDAV error.");
+
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::edit_event()
+ * @return bool
+ */
+ private function _db_edit_event($event)
+ {
+ if (!empty($this->calendars)) {
+ $update_master = false;
+ $update_recurring = true;
+ $old = $this->get_event($event);
+ $ret = true;
+
+ // check if update affects scheduling and update attendee status accordingly
+ $reschedule = $this->_check_scheduling($event, $old, true);
+
+ // increment sequence number
+ if (empty($event['sequence']) && $reschedule)
+ $event['sequence'] = max($event['sequence'], $old['sequence']) + 1;
+
+ // modify a recurring event, check submitted savemode to do the right things
+ if ($old['recurrence'] || $old['recurrence_id']) {
+ $master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old;
+
+ // keep saved exceptions (not submitted by the client)
+ if ($old['recurrence']['EXDATE'])
+ $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
+
+ switch ($event['_savemode']) {
+ case 'new':
+ $event['uid'] = $this->cal->generate_uid();
+ return $this->new_event($event);
+
+ case 'current':
+ // save as exception
+ $event['isexception'] = 1;
+ $update_recurring = false;
+
+ // set exception to first instance (= master)
+ if ($event['id'] == $master['id']) {
+ $event += $old;
+ $event['recurrence_id'] = $master['id'];
+ $event['_instance'] = libcalendaring::recurrence_instance_identifier($old);
+ $event['isexception'] = 1;
+ $event_id = $this->_insert_event($event);
+ return $event_id;
+ }
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event, then save this instance as new recurring event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // if recurrence COUNT, update value to the correct number of future occurences
+ if ($event['recurrence']['COUNT']) {
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $sqlresult = $this->rc->db->query(sprintf(
+ "SELECT event_id FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND %s >= ?
+ AND recurrence_id=?",
+ $this->calendar_ids,
+ $this->rc->db->quote_identifier('start')
+ ),
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']);
+ if ($count = $this->rc->db->num_rows($sqlresult))
+ $event['recurrence']['COUNT'] = $count;
+ }
+
+ $update_recurring = true;
+ $event['recurrence_id'] = 0;
+ $event['isexception'] = 0;
+ $event['_instance'] = '';
+ break;
+ }
+ // else: 'future' == 'all' if modifying the master event
+
+ default: // 'all' is default
+ $event['id'] = $master['id'];
+ $event['recurrence_id'] = 0;
+
+ // use start date from master but try to be smart on time or duration changes
+ $old_start_date = $old['start']->format('Y-m-d');
+ $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
+ $old_duration = $old['end']->format('U') - $old['start']->format('U');
+
+ $new_start_date = $event['start']->format('Y-m-d');
+ $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
+ $new_duration = $event['end']->format('U') - $event['start']->format('U');
+
+ $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+ $date_shift = $old['start']->diff($event['start']);
+
+ // shifted or resized
+ if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
+ $event['start'] = $master['start']->add($old['start']->diff($event['start']));
+ $event['end'] = clone $event['start'];
+ $event['end']->add(new DateInterval('PT' . $new_duration . 'S'));
+ } // dates did not change, use the ones from master
+ else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
+ $event['start'] = $master['start'];
+ $event['end'] = $master['end'];
+ }
+
+ // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+ if (is_array($event['recurrence']) && ($old_start_date != $new_start_date || $old_start_time != $new_start_time)
+ && ($exceptions = $this->_load_exceptions($old))
+ ) {
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ foreach ($exceptions as $exception) {
+ $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+ if (is_a($recurrence_id, 'DateTime')) {
+ $recurrence_id->add($date_shift);
+ $exception['_instance'] = $recurrence_id->format($recurrence_id_format);
+ $this->_update_event($exception, false);
+ }
+ }
+ }
+
+ $ret = $event['id']; // return master ID
+ break;
+ }
+ }
+
+ $success = $this->_update_event($event, $update_recurring);
+
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ $update_event = $event;
+
+ // apply changes to master (and all exceptions)
+ if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
+ $update_event = $this->get_event(array('id' => $event['recurrence_id']));
+ $update_event['_savemode'] = $event['_savemode'];
+ calendar::merge_attendee_data($update_event, $attendees);
+ }
+
+ if ($ret = $this->update_attendees($update_event, $attendees)) {
+ // replace $event with effectively updated event (for iTip reply)
+ if ($ret !== true && $ret != $update_event['id'] && ($new_event = $this->get_event(array('id' => $ret)))) {
+ $event = $new_event;
+ } else {
+ $event = $update_event;
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Update the participant status for the given attendees
+ *
+ * @see calendar_driver::update_attendees()
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ $success = $this->edit_event($event, true);
+
+ // apply attendee updates to recurrence exceptions too
+ if ($success && $event['_savemode'] == 'all' && !empty($event['recurrence']) && empty($event['recurrence_id']) && ($exceptions = $this->_load_exceptions($event))) {
+ foreach ($exceptions as $exception) {
+ calendar::merge_attendee_data($exception, $attendees);
+ $this->_update_event($exception, false);
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Determine whether the current change affects scheduling and reset attendee status accordingly
+ */
+ private function _check_scheduling(&$event, $old, $update = true)
+ {
+ // skip this check when importing iCal/iTip events
+ if (isset($event['sequence']) || !empty($event['_method'])) {
+ return false;
+ }
+
+ $reschedule = false;
+
+ // iterate through the list of properties considered 'significant' for scheduling
+ foreach (self::$scheduling_properties as $prop) {
+ $a = $old[$prop];
+ $b = $event[$prop];
+ if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ $a = $a->format('Y-m-d');
+ $b = $b->format('Y-m-d');
+ }
+ if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
+ unset($a['EXCEPTIONS'], $b['EXCEPTIONS']);
+ $a = array_filter($a);
+ $b = array_filter($b);
+
+ // advanced rrule comparison: no rescheduling if series was shortened
+ if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) {
+ unset($a['COUNT'], $b['COUNT']);
+ } else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) {
+ unset($a['UNTIL'], $b['UNTIL']);
+ }
+ }
+ if ($a != $b) {
+ $reschedule = true;
+ break;
+ }
+ }
+
+ // reset all attendee status to needs-action (#4360)
+ if ($update && $reschedule && is_array($event['attendees'])) {
+ $is_organizer = false;
+ $emails = $this->cal->get_user_emails();
+ $attendees = $event['attendees'];
+ foreach ($attendees as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $is_organizer = true;
+ } else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
+ $attendees[$i]['status'] = 'NEEDS-ACTION';
+ $attendees[$i]['rsvp'] = true;
+ }
+ }
+
+ // update attendees only if I'm the organizer
+ if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
+ $event['attendees'] = $attendees;
+ }
+ }
+
+ return $reschedule;
+ }
+
+ /**
+ * Convert save data to be used in SQL statements
+ */
+ private function _save_preprocess($event)
+ {
+ // shift dates to server's timezone (except for all-day events)
+ if (!$event['allday']) {
+ $event['start'] = clone $event['start'];
+ $event['start']->setTimezone($this->server_timezone);
+ $event['end'] = clone $event['end'];
+ $event['end']->setTimezone($this->server_timezone);
+ }
+
+ // compose vcalendar-style recurrencue rule from structured data
+ $rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : '';
+ $event['_recurrence'] = rtrim($rrule, ';');
+ $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
+ $event['sensitivity'] = intval($this->sensitivity_map[strtolower($event['sensitivity'])]);
+
+ if ($event['free_busy'] == 'tentative') {
+ $event['status'] = 'TENTATIVE';
+ }
+
+ if (isset($event['allday'])) {
+ $event['all_day'] = $event['allday'] ? 1 : 0;
+ }
+
+ // compute absolute time to notify the user
+ $event['notifyat'] = $this->_get_notification($event);
+
+ if (is_array($event['valarms'])) {
+ $event['alarms'] = $this->serialize_alarms($event['valarms']);
+ }
+
+ // process event attendees
+ if (!empty($event['attendees']))
+ $event['attendees'] = json_encode((array)$event['attendees']);
+ else
+ $event['attendees'] = '';
+
+ return $event;
+ }
+
+ /**
+ * Compute absolute time to notify the user
+ */
+ private function _get_notification($event)
+ {
+ if ($event['valarms'] && $event['start'] > new DateTime()) {
+ $alarm = libcalendaring::get_next_alarm($event);
+
+ if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types))
+ return date('Y-m-d H:i:s', $alarm['time']);
+ }
+
+ return null;
+ }
+
+ /**
+ * Save the given event record to database
+ *
+ * @param array Event data
+ * @param boolean True if recurring events instances should be updated, too
+ */
+ private function _update_event($event, $update_recurring = true)
+ {
+ $event = $this->_save_preprocess($event);
+ $sql_set = array();
+ $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'isexception', 'sequence', 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat', 'caldav_url', 'caldav_tag');
+ foreach ($set_cols as $col) {
+ if (is_object($event[$col]) && is_a($event[$col], 'DateTime'))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]->format(self::DB_DATE_FORMAT));
+ else if (is_array($event[$col]))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote(join(',', $event[$col]));
+ else if (array_key_exists($col, $event))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
+ }
+
+ if ($event['_recurrence'])
+ $sql_set[] = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event['_recurrence']);
+
+ if ($event['_instance'])
+ $sql_set[] = $this->rc->db->quote_identifier('instance') . '=' . $this->rc->db->quote($event['_instance']);
+
+ if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar'])
+ $sql_set[] = 'calendar_id=' . $this->rc->db->quote($event['calendar']);
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s %s
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now(),
+ ($sql_set ? ', ' . join(', ', $sql_set) : '')
+ ),
+ $event['id']
+ );
+
+ $success = $this->rc->db->affected_rows($query);
+
+ // add attachments
+ if ($success && !empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event['id']);
+ unset($attachment);
+ }
+ }
+
+ // remove attachments
+ if ($success && !empty($event['deleted_attachments'])) {
+ foreach ($event['deleted_attachments'] as $attachment) {
+ $this->remove_attachment($attachment, $event['id']);
+ }
+ }
+
+ if ($success) {
+ unset($this->cache[$event['id']]);
+ if ($update_recurring)
+ $this->_update_recurring($event);
+ }
+
+ return $success;
+ }
+
+ /**
+ * Insert "fake" entries for recurring occurences of this event
+ */
+ private function _update_recurring($event)
+ {
+ if (empty($this->calendars))
+ return;
+
+ if (!empty($event['recurrence'])) {
+ $exdata = array();
+ $exceptions = $this->_load_exceptions($event);
+
+ foreach ($exceptions as $exception) {
+ $exdate = substr($exception['_instance'], 0, 8);
+ $exdata[$exdate] = $exception;
+ }
+ }
+
+ // clear existing recurrence copies
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE recurrence_id=?
+ AND isexception=0
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $event['id']
+ );
+
+ // create new fake entries
+ if (!empty($event['recurrence'])) {
+ // include library class
+ require_once($this->cal->home . '/lib/calendar_recurrence.php');
+
+ $recurrence = new calendar_recurrence($this->cal, $event);
+
+ $count = 0;
+ $event['allday'] = $event['all_day'];
+ $duration = $event['start']->diff($event['end']);
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ while ($next_start = $recurrence->next_start()) {
+ $instance = $next_start->format($recurrence_id_format);
+ $datestr = substr($instance, 0, 8);
+
+ // skip exceptions
+ // TODO: merge updated data from master event
+ if ($exdata[$datestr]) {
+ continue;
+ }
+
+ $next_start->setTimezone($this->server_timezone);
+ $next_end = clone $next_start;
+ $next_end->add($duration);
+
+ $notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_start, 'end' => $next_end, 'status' => $event['status']));
+ $query = $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, recurrence_id, created, changed, uid, instance, %s, %s, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, notifyat, caldav_url, caldav_tag)
+ SELECT calendar_id, ?, %s, %s, uid, ?, ?, ?, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, ?, caldav_url, caldav_tag
+ FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['id'],
+ $instance,
+ $next_start->format(self::DB_DATE_FORMAT),
+ $next_end->format(self::DB_DATE_FORMAT),
+ $notify_at,
+ $event['id']
+ );
+
+ if (!$this->rc->db->affected_rows($query))
+ break;
+
+ // stop adding events for inifinite recurrence after 20 years
+ if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20))
+ break;
+ }
+
+ // remove all exceptions after recurrence end
+ if ($next_end && !empty($exceptions)) {
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `start` > ?
+ AND `calendar_id` IN (" . $this->calendar_ids . ")",
+ $event['id'],
+ $next_end->format(self::DB_DATE_FORMAT)
+ );
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ private function _load_exceptions($event, $instance_id = null)
+ {
+ $sql_add_where = '';
+ if (!empty($instance_id)) {
+ $sql_add_where = 'AND `instance`=?';
+ }
+
+ $result = $this->rc->db->query(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `calendar_id` IN (" . $this->calendar_ids . ")
+ $sql_add_where
+ ORDER BY `instance`, `start`",
+ $event['id'],
+ $instance_id
+ );
+
+ $exceptions = array();
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $exception = $this->_read_postprocess($sql_arr);
+ $instance = $exception['_instance'] ?: $exception['start']->format($exception['allday'] ? 'Ymd' : 'Ymd\THis');
+ $exceptions[$instance] = $exception;
+ }
+
+ return $exceptions;
+ }
+
+ /**
+ * Move a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::move_event()
+ * @return bool
+ */
+ public function move_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Resize a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::resize_event()
+ * @return bool
+ */
+ public function resize_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Remove a single event from the database and from the CalDAV server.
+ *
+ * @param array Hash array with event properties
+ * @param boolean Remove record irreversible
+ *
+ * @see calendar_driver::remove_event()
+ * @return bool
+ */
+ public function remove_event($event, $force = true)
+ {
+ $event_id = (int)$event["id"];
+ $cal_id = (int)$event["calendar"];
+ $event = $this->get_event($event);
+
+ $sync_client = $this->sync_clients[$cal_id];
+ $success = $sync_client->remove_event($event);
+
+ if($success === true)
+ {
+ $this->_db_remove_event($event, $force);
+ self::debug_log("Successfully removed event \"$event_id\".");
+
+ // Trigger calendar sync to update ctags and etags.
+ $this->_sync_calendar($cal_id);
+
+ return true;
+ }
+
+ $this->_raise_error("Could not remove event: Unexpected CalDAV error.");
+ return false;
+ }
+
+ /**
+ * Remove a single event from the database
+ *
+ * @param array Hash array with event properties
+ * @param boolean Remove record irreversible (@TODO)
+ *
+ * @see calendar_driver::remove_event()
+ * @return bool
+ */
+ private function _db_remove_event($event, $force = true)
+ {
+ if (!empty($this->calendars)) {
+ $event += (array)$this->get_event($event);
+ $master = $event;
+ $update_master = false;
+ $savemode = 'all';
+ $ret = true;
+
+ // read master if deleting a recurring event
+ if ($event['recurrence'] || $event['recurrence_id']) {
+ $master = $event['recurrence_id'] ? $this->get_event(array('id' => $event['recurrence_id'])) : $event;
+ $savemode = $event['_savemode'];
+ }
+
+ switch ($savemode) {
+ case 'current':
+ // add exception to master event
+ $master['recurrence']['EXDATE'][] = $event['start'];
+ $update_master = true;
+
+ // just delete this single occurence
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND event_id=?",
+ $event['id']
+ );
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // delete this and all future instances
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND " . $this->rc->db->quote_identifier('start') . " >= ?
+ AND recurrence_id=?",
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']
+ );
+ $ret = $master['id'];
+ break;
+ }
+ // else: future == all if modifying the master event
+
+ default: // 'all' is default
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE (event_id=? OR recurrence_id=?)
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $master['id'],
+ $master['id']
+ );
+ break;
+ }
+
+ $success = $this->rc->db->affected_rows($query);
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return data of a specific event
+ * @param mixed Hash array with event properties or event UID
+ * @param integer Bitmask defining the scope to search events in
+ * @param boolean If true, recurrence exceptions shall be added
+ * @return array Hash array with event properties
+ */
+ public function get_event($event, $scope = 0, $full = false)
+ {
+ $id = is_array($event) ? ($event['id'] ?: $event['uid']) : $event;
+ $cal = is_array($event) ? $event['calendar'] : null;
+ $col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid';
+
+ $where_add = '';
+ if (is_array($event) && !$event['id'] && !empty($event['_instance'])) {
+ $where_add = 'AND instance=' . $this->rc->db->quote($event['_instance']);
+ }
+
+ if ($this->cache[$id])
+ return $this->cache[$id];
+
+ if ($scope & self::FILTER_ACTIVE) {
+ $calendars = $this->calendars;
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ $cals = join(',', $calendars);
+ } else {
+ $cals = $this->calendar_ids;
+ }
+
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " AS e
+ WHERE e.calendar_id IN (%s)
+ AND e.$col=?
+ %s",
+ $cals,
+ $where_add
+ ),
+ $id);
+
+ if ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $event = $this->_read_postprocess($sql_arr);
+
+ // also load recurrence exceptions
+ if (!empty($event['recurrence']) && $full) {
+ $event['recurrence']['EXCEPTIONS'] = array_values($this->_load_exceptions($event));
+ }
+
+ $this->cache[$id] = $event;
+ return $this->cache[$id];
+ }
+
+ return false;
+ }
+
+ /**
+ * Sync and returns event data
+ *
+ * @see calendar_driver::load_events()
+ */
+ public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (!is_array($calendars))
+ $calendars = explode(',', strval($calendars));
+
+ // only allow to select from calendars of this use
+ $calendar_ids = array_intersect($calendars, array_keys($this->calendars));
+
+ // Make sure that the calendars are in sync.
+ foreach ($calendar_ids as $cal_id) {
+ if (!$this->_is_synced($cal_id))
+ $this->_sync_calendar($cal_id);
+ }
+
+ return $this->_db_load_events($start, $end, $query, $calendars, $virtual, $modifiedsince);
+ }
+
+ /**
+ * Get event data
+ *
+ * @see calendar_driver::load_events()
+ */
+ private function _db_load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (!is_array($calendars))
+ $calendars = explode(',', strval($calendars));
+
+ // only allow to select from calendars of this use
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars)));
+
+ // compose (slow) SQL query for searching
+ // FIXME: improve searching using a dedicated col and normalized values
+ if ($query) {
+ foreach (array('title', 'location', 'description', 'categories', 'attendees') as $col)
+ $sql_query[] = $this->rc->db->ilike($col, '%' . $query . '%');
+ $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
+ }
+
+ if (!$virtual)
+ $sql_add .= ' AND e.recurrence_id = 0';
+
+ if ($modifiedsince)
+ $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince));
+
+ $events = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " e
+ WHERE e.calendar_id IN (%s)
+ AND e.start <= %s AND e.end >= %s
+ %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($end),
+ $this->rc->db->fromunixtime($start),
+ $sql_add
+ ));
+
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result))) {
+ $event = $this->_read_postprocess($sql_arr);
+ $add = true;
+
+ if (!empty($event['recurrence']) && !$event['recurrence_id']) {
+ // load recurrence exceptions (i.e. for export)
+ if (!$virtual) {
+ $event['recurrence']['EXCEPTIONS'] = $this->_load_exceptions($event);
+ } // check for exception on first instance
+ else {
+ $instance = libcalendaring::recurrence_instance_identifier($event);
+ $exceptions = $this->_load_exceptions($event, $instance);
+ if ($exceptions && is_array($exceptions[$instance])) {
+ $event = $exceptions[$instance];
+ $add = false;
+ }
+ }
+ }
+
+ if ($add)
+ $events[] = $event;
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ public function count_events($calendars, $start, $end = null)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Convert sql record into a rcube style event object
+ */
+ private function _read_postprocess($event)
+ {
+ $free_busy_map = array_flip($this->free_busy_map);
+ $sensitivity_map = array_flip($this->sensitivity_map);
+
+ $event['id'] = $event['event_id'];
+ $event['start'] = new DateTime($event['start']);
+ $event['end'] = new DateTime($event['end']);
+ $event['allday'] = intval($event['all_day']);
+ $event['created'] = new DateTime($event['created']);
+ $event['changed'] = new DateTime($event['changed']);
+ $event['free_busy'] = $free_busy_map[$event['free_busy']];
+ $event['sensitivity'] = $sensitivity_map[$event['sensitivity']];
+ $event['calendar'] = $event['calendar_id'];
+ $event['recurrence_id'] = intval($event['recurrence_id']);
+ $event['isexception'] = intval($event['isexception']);
+
+ // parse recurrence rule
+ if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) {
+ $event['recurrence'] = array();
+ foreach ($m as $rr) {
+ if (is_numeric($rr[2]))
+ $rr[2] = intval($rr[2]);
+ else if ($rr[1] == 'UNTIL')
+ $rr[2] = date_create($rr[2]);
+ else if ($rr[1] == 'RDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ else if ($rr[1] == 'EXDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ $event['recurrence'][$rr[1]] = $rr[2];
+ }
+ }
+
+ if ($event['recurrence_id']) {
+ libcalendaring::identify_recurrence_instance($event);
+ }
+
+ if (strlen($event['instance'])) {
+ $event['_instance'] = $event['instance'];
+
+ if (empty($event['recurrence_id'])) {
+ $event['recurrence_date'] = rcube_utils::anytodatetime($event['_instance'], $event['start']->getTimezone());
+ }
+ }
+
+ if ($event['_attachments'] > 0) {
+ $event['attachments'] = (array)$this->list_attachments($event);
+ }
+
+ // decode serialized event attendees
+ if (strlen($event['attendees'])) {
+ $event['attendees'] = $this->unserialize_attendees($event['attendees']);
+ } else {
+ $event['attendees'] = array();
+ }
+
+ // decode serialized alarms
+ if ($event['alarms']) {
+ $event['valarms'] = $this->unserialize_alarms($event['alarms']);
+ }
+
+ unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['instance'], $event['_attachments']);
+ return $event;
+ }
+
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @see calendar_driver::pending_alarms()
+ */
+ public function pending_alarms($time, $calendars = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (is_string($calendars))
+ $calendars = explode(',', $calendars);
+
+ // only allow to select from calendars with activated alarms
+ $calendar_ids = array();
+ foreach ($calendars as $cid) {
+ if ($this->calendars[$cid] && $this->calendars[$cid]['showalarms'])
+ $calendar_ids[] = $cid;
+ }
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids);
+
+ $alarms = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND notifyat <= %s AND %s > %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($time),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->fromunixtime($time)
+ ));
+
+ while ($result && ($event = $this->rc->db->fetch_assoc($result)))
+ $alarms[] = $this->_read_postprocess($event);
+ }
+
+ return $alarms;
+ }
+
+ /**
+ * Feedback after showing/sending an alarm notification
+ *
+ * @see calendar_driver::dismiss_alarm()
+ */
+ public function dismiss_alarm($event_id, $snooze = 0)
+ {
+ // set new notifyat time or unset if not snoozed
+ $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null;
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s, notifyat=?
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now()),
+ $notify_at,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Save an attachment related to the given event
+ */
+ private function add_attachment($attachment, $event_id)
+ {
+ $data = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
+
+ $query = $this->rc->db->query(
+ "INSERT INTO " . $this->db_attachments .
+ " (event_id, filename, mimetype, size, data)" .
+ " VALUES (?, ?, ?, ?, ?)",
+ $event_id,
+ $attachment['name'],
+ $attachment['mimetype'],
+ strlen($data),
+ base64_encode($data)
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Remove a specific attachment from the given event
+ */
+ private function remove_attachment($attachment_id, $event_id)
+ {
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_attachments .
+ " WHERE attachment_id = ?" .
+ " AND event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id = ?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))",
+ $attachment_id,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * List attachments of specified event
+ */
+ public function list_attachments($event)
+ {
+ $attachments = array();
+
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id=?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))".
+ " ORDER BY filename",
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id']
+ );
+
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $attachments[] = $arr;
+ }
+ }
+
+ return $attachments;
+ }
+
+ /**
+ * Get attachment properties
+ */
+ public function get_attachment($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?",
+ $id,
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return $arr;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get attachment body
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT data " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?",
+ $id,
+ $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return base64_decode($arr['data']);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Remove the given category
+ */
+ public function remove_category($name)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=''
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Update/replace a category
+ */
+ public function replace_category($oldname, $name, $color)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=?
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name,
+ $oldname
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Helper method to serialize the list of alarms into a string
+ */
+ private function serialize_alarms($valarms)
+ {
+ foreach ((array)$valarms as $i => $alarm) {
+ if ($alarm['trigger'] instanceof DateTime) {
+ $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c');
+ }
+ }
+
+ return $valarms ? json_encode($valarms) : null;
+ }
+
+ /**
+ * Helper method to decode a serialized list of alarms
+ */
+ private function unserialize_alarms($alarms)
+ {
+ // decode json serialized alarms
+ if ($alarms && $alarms[0] == '[') {
+ $valarms = json_decode($alarms, true);
+ foreach ($valarms as $i => $alarm) {
+ if ($alarm['trigger'][0] == '@') {
+ try {
+ $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1));
+ }
+ catch (Exception $e) {
+ unset($valarms[$i]);
+ }
+ }
+ }
+ }
+ // convert legacy alarms data
+ else if (strlen($alarms)) {
+ list($trigger, $action) = explode(':', $alarms, 2);
+ if ($trigger = libcalendaring::parse_alarm_value($trigger)) {
+ $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0]));
+ }
+ }
+
+ return $valarms;
+ }
+
+ /**
+ * Helper method to decode the attendees list from string
+ */
+ private function unserialize_attendees($s_attendees)
+ {
+ $attendees = array();
+
+ // decode json serialized string
+ if ($s_attendees[0] == '[') {
+ $attendees = json_decode($s_attendees, true);
+ } // decode the old serialization format
+ else {
+ foreach (explode("\n", $event['attendees']) as $line) {
+ $att = array();
+ foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) {
+ list($key, $value) = explode("=", $prop);
+ $att[strtolower($key)] = stripslashes(trim($value, '""'));
+ }
+ $attendees[] = $att;
+ }
+ }
+
+ return $attendees;
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ $db = $this->rc->db;
+ $user = $args['user'];
+ $event_ids = array();
+
+ $events = $db->query(
+ "SELECT event_id FROM " . $this->db_events . " AS ev" .
+ " LEFT JOIN " . $this->db_calendars . " cal ON (ev.calendar_id = cal.calendar_id)".
+ " WHERE user_id=?",
+ $user->ID);
+
+ while ($row = $db->fetch_assoc($events)) {
+ $event_ids[] = $row['event_id'];
+ }
+
+ if (!empty($event_ids)) {
+ foreach (array($this->db_attachments, $this->db_events) as $table) {
+ $db->query(sprintf("DELETE FROM $table WHERE event_id IN (%s)", join(',', $event_ids)));
+ }
+ }
+
+ foreach (array($this->db_calendars, 'itipinvitations') as $table) {
+ $db->query("DELETE FROM $table WHERE user_id=?", $user->ID);
+ }
+ }
+
+ /**
+ * Callback function to produce driver-specific calendar create/edit form
+ *
+ * @param string Request action 'form-edit|form-new'
+ * @param array Calendar properties (e.g. id, color)
+ * @param array Edit form fields
+ *
+ * @return string HTML content of the form
+ */
+ public function calendar_form($action, $calendar, $formfields)
+ {
+ // Make sure we have current attributes
+ $calendar = $this->calendars[$calendar["id"]];
+
+ $input_caldav_url = new html_inputfield( array(
+ "name" => "caldav_url",
+ "id" => "caldav_url",
+ "size" => 20
+ ));
+
+ $formfields["caldav_url"] = array(
+ "label" => $this->cal->gettext("caldavurl"),
+ "value" => $input_caldav_url->show($calendar["caldav_url"]),
+ "id" => "caldav_url",
+ );
+
+ $input_caldav_user = new html_inputfield( array(
+ "name" => "caldav_user",
+ "id" => "caldav_user",
+ "size" => 20
+ ));
+
+ $formfields["caldav_user"] = array(
+ "label" => $this->cal->gettext("username"),
+ "value" => $input_caldav_user->show($calendar["caldav_user"]),
+ "id" => "caldav_user",
+ );
+
+ $input_caldav_pass = new html_passwordfield( array(
+ "name" => "caldav_pass",
+ "id" => "caldav_pass",
+ "size" => 20
+ ));
+
+ $formfields["caldav_pass"] = array(
+ "label" => $this->cal->gettext("password"),
+ "value" => $input_caldav_pass->show(null), // Don't send plain text password to GUI
+ "id" => "caldav_pass",
+ );
+
+ return parent::calendar_form($action, $calendar, $formfields);
+ }
+
+ /**
+ * Encodes directory- and filenames using rawurlencode().
+ *
+ * @see http://stackoverflow.com/questions/7973790/urlencode-only-the-directory-and-file-names-of-a-url
+ * @param string Unencoded URL to be encoded.
+ * @return Encoded URL.
+ */
+ private static function _encode_url($url)
+ {
+ // Don't encode if "%" is already used.
+ if(strstr($url, "%") === false)
+ {
+ return preg_replace_callback('#://([^/]+)/([^?]+)#', function ($matches) {
+ return '://' . $matches[1] . '/' . join('/', array_map('rawurlencode', explode('/', $matches[2])));
+ }, $url);
+ }
+ else return $url;
+ }
+
+ /**
+ * Expand all "%p" occurrences in 'caldav_pass' element of calendar object
+ * properties array with RC (imap) password.
+ * Other elements are left untouched.
+ *
+ * @param array List of properties
+ * @return array List of properties, with expanded 'caldav_pass' attribute
+ *
+ */
+ private function _expand_pass($props)
+ {
+ if (isset($props['caldav_pass']))
+ $props['caldav_pass'] = str_replace('%p', $this->rc->get_user_password(), $props['caldav_pass']);
+
+ return $props;
+ }
+
+ /**
+ * Auto discover calenders available to the user on the caldav server
+ * @param array $props
+ * caldav_url: Absolute URL to CalDAV server
+ * caldav_user: Username
+ * caldav_pass: Password
+ * @return False on error or an array with the following calendar props:
+ * name: Calendar display name
+ * href: Absolute calendar URL
+ */
+ private function _autodiscover_calendars($props)
+ {
+ $calendars = array();
+ $current_user_principal = array('{DAV:}current-user-principal');
+ $calendar_home_set = array('{urn:ietf:params:xml:ns:caldav}calendar-home-set');
+ $cal_attribs = array('{DAV:}resourcetype', '{DAV:}displayname');
+
+ require_once ($this->cal->home.'/lib/caldav-client.php');
+ $caldav = new caldav_client($props["caldav_url"], $props["caldav_user"], $props["caldav_pass"]);
+
+ $tokens = parse_url($props["caldav_url"]);
+ $base_uri = $tokens['scheme']."://".$tokens['host'].($tokens['port'] ? ":".$tokens['port'] : null);
+ $caldav_url = $props["caldav_url"];
+ $response = $caldav->prop_find($caldav_url, array_merge($current_user_principal,$cal_attribs), 0);
+ if (!$response) {
+ $this->_raise_error("Resource \"$caldav_url\" has no collections");
+ return false;
+ }
+ else if (array_key_exists ('{DAV:}resourcetype', $response) &&
+ $response['{DAV:}resourcetype'] instanceof OldSabre\DAV\Property\ResourceType &&
+ in_array('{urn:ietf:params:xml:ns:caldav}calendar',
+ $response['{DAV:}resourcetype']->getValue())) {
+
+ $name = '';
+ if (array_key_exists ('{DAV:}displayname', $response)) {
+ $name = $response['{DAV:}displayname'];
+ }
+
+ array_push($calendars, array(
+ 'name' => $name,
+ 'href' => $caldav_url,
+ ));
+ return $calendars;
+ // directly return given url as it is a calendar
+ }
+ // probe further for principal url and user home set
+ $caldav_url = $base_uri . $response[$current_user_principal[0]];
+ $response = $caldav->prop_find($caldav_url, $calendar_home_set, 0);
+ if (!$response) {
+ $this->_raise_error("Resource \"$caldav_url\" contains no calendars.");
+ return false;
+ }
+ $caldav_url = $base_uri . $response[$calendar_home_set[0]];
+ $response = $caldav->prop_find($caldav_url, $cal_attribs, 1);
+ foreach($response as $collection => $attribs)
+ {
+ $found = false;
+ $name = '';
+ foreach($attribs as $key => $value)
+ {
+ if ($key == '{DAV:}resourcetype' && is_object($value)) {
+ if ($value instanceof OldSabre\DAV\Property\ResourceType) {
+ $values = $value->getValue();
+ if (in_array('{urn:ietf:params:xml:ns:caldav}calendar', $values))
+ $found = true;
+ }
+ }
+ else if ($key == '{DAV:}displayname') {
+ $name = $value;
+ }
+ }
+ if ($found) {
+ array_push($calendars, array(
+ 'name' => $name,
+ 'href' => $base_uri.$collection,
+ ));
+ }
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Synchronizes events of given calendar.
+ *
+ * @param int Calendar ID to sync
+ */
+ private function _sync_calendar($cal_id)
+ {
+ self::debug_log("Syncing calendar id \"$cal_id\".");
+
+ $cal_sync = $this->sync_clients[$cal_id];
+ $events = array();
+
+ // Ignore recurrence events and read caldav props
+ foreach($this->_load_all_events($cal_id) as $event) {
+ if($event["recurrence_id"] == 0) {
+ array_push($events, $event);
+ }
+ }
+
+ $updates = $cal_sync->get_updates($events);
+ if($updates)
+ {
+ list($updates, $synced_event_ids) = $updates;
+ $updated_event_ids = $this->_perform_updates($updates);
+
+ // Delete events that are not in sync or updated.
+ foreach($events as $event)
+ {
+ if(array_search($event["id"], $updated_event_ids) === false && // No updated event
+ array_search($event["id"], $synced_event_ids) === false) // No in-sync event
+ {
+ // Assume: Event not in sync and not updated, so delete!
+ $this->_db_remove_event($event, true);
+ self::debug_log("Remove event \"".$event["id"]."\".");
+ }
+ }
+
+ // Update calendar ctag ...
+ $calendar = $this->calendars[$cal_id];
+ $calendar["caldav_tag"] = $cal_sync->get_ctag();
+ $this->edit_calendar($calendar);
+ }
+
+ self::debug_log("Successfully synced calendar id \"$cal_id\".");
+ }
+
+ /**
+ * Return all events from the given calendar.
+ *
+ * @param int Calendar id.
+ * @return array
+ */
+ private function _load_all_events($cal_id)
+ {
+ // FIXME: This is kind of ugly but a way to get _all_ events without touching the database driver.
+
+ // Get the event with the maximum end time.
+ $result = $this->rc->db->query(
+ "SELECT MAX(e.end) as end FROM ".$this->db_events." e ".
+ "WHERE e.calendar_id = ? ", $cal_id);
+
+ if($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $end = new DateTime($arr["end"]);
+
+ // Don't use load_events() which is doing another sync while this method might be already invoked in an sync.
+ return $this->_db_load_events(0, $end->getTimestamp(), null, array($cal_id));
+ }
+ else return array();
+ }
+
+ /**
+ * Performs caldav updates on given events.
+ *
+ * @param array Caldav and event properties to update. See caldav_sync::get_updates().
+ * @return array List of event ids.
+ */
+ private function _perform_updates($updates)
+ {
+ $event_ids = array();
+
+ $num_created = 0;
+ $num_updated = 0;
+
+ foreach($updates as $update)
+ {
+ // local event -> update event
+ if(isset($update["local_event"]))
+ {
+ // Overwrite local event attributes with new event, url + etag.
+ $event = array_merge((array)$update["local_event"], $update["remote_event"], array(
+ "caldav_url" => $update["url"],
+ "caldav_tag" => $update["etag"]));
+
+ // let edit_event() do all the magic
+ if($this->_db_edit_event($event))
+ {
+ $event_id = $event["id"];
+ array_push($event_ids, $event_id);
+ $num_updated ++;
+ }
+ else
+ {
+ self::debug_log("Could not perform event update: ".print_r($update, true));
+ }
+ }
+
+ // no local event -> create event
+ else
+ {
+ $event = array_merge($update["remote_event"], array(
+ "caldav_url" => $update["url"],
+ "caldav_tag" => $update["etag"]));
+
+ $event_id = $this->new_event($event);
+ if($event_id)
+ {
+ self::debug_log("Created event \"$event_id\".");
+ array_push($event_ids, $event_id);
+ $num_created ++;
+ }
+ else
+ {
+ self::debug_log("Could not perform event creation: ".print_r($update, true));
+ }
+ }
+ }
+
+ self::debug_log("Created $num_created new events, updated $num_updated event.");
+ return $event_ids;
+ }
+
+ /**
+ * Determines whether the given calendar is in sync regarding
+ * calendar's ctag and the configured sync period.
+ *
+ * @param int Calender id.
+ * @return boolean True if calendar is in sync, true otherwise.
+ */
+ private function _is_synced($cal_id)
+ {
+ // Atomic sql: Check for exceeded sync period and update last_change.
+ $query = $this->rc->db->query(
+ "UPDATE ".$this->db_calendars." ".
+ "SET caldav_last_change = NOW() WHERE calendar_id = ? AND ".
+ $this->_unix_timestamp('caldav_last_change') ." + ? <= ".$this->_unix_timestamp('NOW()'),
+ $cal_id, $this->sync_period);
+
+ if($query->rowCount() > 0)
+ {
+ $is_synced = $this->sync_clients[$cal_id]->is_synced();
+ self::debug_log("Calendar \"$cal_id\" ".($is_synced ? "is in sync" : "needs update").".");
+ return $is_synced;
+ }
+ else
+ {
+ self::debug_log("Sync period active: Assuming calendar \"$cal_id\" to be in sync.");
+ return true;
+ }
+ }
+
+ /**
+ * Returns db-specific timestamp queries for epoch format
+ *
+ * @param str column name or valid timestamp (e.g. NOW())
+ * @return str db-specific timestamp query for epoch format
+ */
+ private function _unix_timestamp($field)
+ {
+ switch ($this->rc->db->db_provider) {
+ case 'postgres':
+ return "EXTRACT (EPOCH FROM $field)";
+ default:
+ return "UNIX_TIMESTAMP($field)";
+ }
+ }
+
+ private function _decrypt_pass($pass) {
+ $p = base64_decode($pass);
+ $e = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC);
+ return $e->decrypt($p, $this->crypt_key);
+ }
+
+ private function _encrypt_pass($pass) {
+ $e = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC);
+ $p = $e->encrypt($pass, $this->crypt_key);
+ return base64_encode($p);
+ }
+}
diff --git a/calendar/drivers/caldav/caldav_sync.php b/calendar/drivers/caldav/caldav_sync.php
new file mode 100644
index 0000000..efe92c2
--- /dev/null
+++ b/calendar/drivers/caldav/caldav_sync.php
@@ -0,0 +1,253 @@
+<?php
+/**
+ * CalDAV sync for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once (dirname(__FILE__).'/../../lib/caldav-client.php');
+
+class caldav_sync
+{
+ const ACTION_NONE = 1;
+ const ACTION_UPDATE = 2;
+ const ACTION_CREATE = 4;
+
+ private $cal_id = null;
+ private $ctag = null;
+ private $username = null;
+ private $pass = null;
+ private $url = null;
+
+ /**
+ * Default constructor for calendar synchronization adapter.
+ *
+ * @param array Hash array with caldav properties at least the following:
+ * id: Calendar ID
+ * caldav_url: Caldav calendar URL.
+ * caldav_user: Caldav http basic auth user.
+ * caldav_pass: Password für caldav user.
+ * caldav_tag: Caldav ctag for calendar.
+ */
+ public function __construct($cal)
+ {
+ $this->cal_id = $cal["id"];
+ $this->url = $cal["caldav_url"];
+ $this->ctag = isset($cal["caldav_tag"]) ? $cal["caldav_tag"] : null;
+ $this->username = isset($cal["caldav_user"]) ? $cal["caldav_user"] : null;
+ $this->pass = isset($cal["caldav_pass"]) ? $cal["caldav_pass"] : null;
+
+ $this->caldav = new caldav_client($this->url, $this->username, $this->pass);
+ }
+
+ /**
+ * Getter for current calendar ctag.
+ * @return string
+ */
+ public function get_ctag()
+ {
+ return $this->ctag;
+ }
+
+ /**
+ * Determines whether current calendar needs to be synced
+ * regarding the CalDAV ctag.
+ *
+ * @return True if the current calendar ctag differs from the CalDAV tag which
+ * indicates that there are changes that must be synched. Returns false
+ * if the calendar is up to date, no sync necesarry.
+ */
+ public function is_synced()
+ {
+ $is_synced = $this->ctag == $this->caldav->get_ctag() && $this->ctag;
+ caldav_driver::debug_log("Ctag indicates that calendar \"$this->cal_id\" ".($is_synced ? "is synced." : "needs update!"));
+
+ return $is_synced;
+ }
+
+ /**
+ * Synchronizes given events with caldav server and returns updates.
+ *
+ * @param array List of hash arrays with event properties, must include "caldav_url" and "tag".
+ * @return array Tuple containing the following lists:
+ *
+ * Caldav properties for events to be created or to be updated with the keys:
+ * url: Event ical URL relative to calendar URL
+ * etag: Remote etag of the event
+ * local_event: The local event in case of an update.
+ * remote_event: The current event retrieved from caldav server.
+ *
+ * A list of event ids that are in sync.
+ */
+ public function get_updates($events)
+ {
+ $ctag = $this->caldav->get_ctag();
+
+ if($ctag)
+ {
+ $this->ctag = $ctag;
+ $etags = $this->caldav->get_etags();
+
+ list($updates, $synced_event_ids) = $this->_get_event_updates($events, $etags);
+ return array($this->_get_event_data($updates), $synced_event_ids);
+ }
+ else
+ {
+ caldav_driver::debug_log("Unkown error while fetching calendar ctag for calendar \"$this->cal_id\"!");
+ }
+
+ return null;
+ }
+
+ /**
+ * Determines sync status and requried updates for the given events using given list of etags.
+ *
+ * @param array List of hash arrays with event properties, must include "caldav_url" and "caldav_tag".
+ * @param array List of current remote etags.
+ * @return array Tuple containing the following lists:
+ *
+ * Caldav properties for events to be created or to be updated with the keys:
+ * url: Event ical URL relative to calendar URL
+ * etag: Remote etag of the event
+ * local_event: The local event in case of an update.
+ *
+ * A list of event ids that are in sync.
+ */
+ private function _get_event_updates($events, $etags)
+ {
+ $updates = array();
+ $in_sync = array();
+
+ foreach ($etags as $etag)
+ {
+ $url = $etag["url"];
+ $etag = $etag["etag"];
+ $event_found = false;
+ foreach($events as $event)
+ {
+ if ($event["caldav_url"] == $url)
+ {
+ $event_found = true;
+
+ if ($event["caldav_tag"] != $etag)
+ {
+ caldav_driver::debug_log("Event ".$event["uid"]." needs update.");
+
+ array_push($updates, array(
+ "local_event" => $event,
+ "etag" => $etag,
+ "url" => $url
+ ));
+ }
+ else
+ {
+ array_push($in_sync, $event["id"]);
+ }
+ }
+ }
+
+ if (!$event_found)
+ {
+ caldav_driver::debug_log("Found new event ".$url);
+
+ array_push($updates, array(
+ "url" => $url,
+ "etag" => $etag
+ ));
+ }
+ }
+
+ return array($updates, $in_sync);
+ }
+
+ /**
+ * Fetches event data and attaches it to the given update properties.
+ *
+ * @param $updates List of update properties.
+ * @return array List of update properties with additional key "remote_event" containing the current caldav event.
+ */
+ private function _get_event_data($updates)
+ {
+ $urls = array();
+
+ foreach ($updates as $update)
+ {
+ array_push($urls, $update["url"]);
+ }
+
+ $events = $this->caldav->get_events($urls);
+ foreach($updates as &$update)
+ {
+ // Attach remote events to the appropriate updates.
+ // Note that this assumes unique event URL's!
+ $url = $update["url"];
+ if($events[$url]) {
+ $update["remote_event"] = $events[$url];
+ $update["remote_event"]["calendar"] = $this->cal_id;
+ }
+ }
+
+ return $updates;
+ }
+
+ /**
+ * Creates the given event on the CalDAV server.
+ *
+ * @param array Hash array with event properties.
+ * @return Event with updated "caldav_url" and "caldav_tag" attributes, false on error.
+ */
+ public function create_event($event)
+ {
+ $props = array(
+ "caldav_url" => parse_url($this->url, PHP_URL_PATH)."/".$event["uid"].".ics",
+ "caldav_tag" => null
+ );
+
+ caldav_driver::debug_log("Push new event to url ".$props["caldav_url"]);
+ $result = $this->caldav->put_event($props["caldav_url"], $event);
+
+ if($result == false || $result < 0) return false;
+ return array_merge($event, $props);
+ }
+
+ /**
+ * Updates the given event on the CalDAV server.
+ *
+ * @param array Hash array with event properties to update, must include "uid", "caldav_url" and "caldav_tag".
+ * @return True on success, false on error, -1 if the given event/etag is not up to date.
+ */
+ public function update_event($event)
+ {
+ caldav_driver::debug_log("Updating event uid \"".$event["uid"]."\".");
+ return $this->caldav->put_event($event["caldav_url"], $event, $event["caldav_tag"]);
+ }
+
+ /**
+ * Removes the given event from the caldav server.
+ *
+ * @param array Hash array with events properties, must include "caldav_url".
+ * @return True on success, false on error.
+ */
+ public function remove_event($event)
+ {
+ caldav_driver::debug_log("Removing event uid \"".$event["uid"]."\".");
+ return $this->caldav->remove_event($event["caldav_url"]);
+ }
+};
+?>
diff --git a/calendar/drivers/calendar_driver.php b/calendar/drivers/calendar_driver.php
new file mode 100644
index 0000000..3202003
--- /dev/null
+++ b/calendar/drivers/calendar_driver.php
@@ -0,0 +1,819 @@
+<?php
+
+/**
+ * Driver interface for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+/**
+ * Struct of an internal event object how it is passed from/to the driver classes:
+ *
+ * $event = array(
+ * 'id' => 'Event ID used for editing',
+ * 'uid' => 'Unique identifier of this event',
+ * 'calendar' => 'Calendar identifier to add event to or where the event is stored',
+ * 'start' => DateTime, // Event start date/time as DateTime object
+ * 'end' => DateTime, // Event end date/time as DateTime object
+ * 'allday' => true|false, // Boolean flag if this is an all-day event
+ * 'changed' => DateTime, // Last modification date of event
+ * 'title' => 'Event title/summary',
+ * 'location' => 'Location string',
+ * 'description' => 'Event description',
+ * 'url' => 'URL to more information',
+ * 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs
+ * 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY',
+ * 'INTERVAL' => 1...n,
+ * 'UNTIL' => DateTime,
+ * 'COUNT' => 1..n, // number of times
+ * // + more properties (see http://www.kanzaki.com/docs/ical/recur.html)
+ * 'EXDATE' => array(), // list of DateTime objects of exception Dates/Times
+ * 'EXCEPTIONS' => array(<event>), list of event objects which denote exceptions in the recurrence chain
+ * ),
+ * 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event
+ * '_instance' => 'ID of the recurring instance', // identifies an instance within a recurrence chain
+ * 'categories' => 'Event category',
+ * 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
+ * 'status' => 'TENTATIVE|CONFIRMED|CANCELLED', // event status according to RFC 2445
+ * 'priority' => 0-9, // Event priority (0=undefined, 1=highest, 9=lowest)
+ * 'sensitivity' => 'public|private|confidential', // Event sensitivity
+ * 'alarms' => '-15M:DISPLAY', // DEPRECATED Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event)
+ * 'valarms' => array( // List of reminders (new format), each represented as a hash array:
+ * array(
+ * 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object
+ * 'action' => 'DISPLAY|EMAIL|AUDIO',
+ * 'duration' => 'PT15M', // ISO 8601 period string
+ * 'repeat' => 0, // number of repetitions
+ * 'description' => '', // text to display for DISPLAY actions
+ * 'summary' => '', // message text for EMAIL actions
+ * 'attendees' => array(), // list of email addresses to receive alarm messages
+ * ),
+ * ),
+ * 'attachments' => array( // List of attachments
+ * 'name' => 'File name',
+ * 'mimetype' => 'Content type',
+ * 'size' => 1..n, // in bytes
+ * 'id' => 'Attachment identifier'
+ * ),
+ * 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated
+ * 'attendees' => array( // List of event participants
+ * 'name' => 'Participant name',
+ * 'email' => 'Participant e-mail address', // used as identifier
+ * 'role' => 'ORGANIZER|REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR',
+ * 'status' => 'NEEDS-ACTION|UNKNOWN|ACCEPTED|TENTATIVE|DECLINED'
+ * 'rsvp' => true|false,
+ * ),
+ *
+ * '_savemode' => 'all|future|current|new', // How changes on recurring event should be handled
+ * '_notify' => true|false, // whether to notify event attendees about changes
+ * '_fromcalendar' => 'Calendar identifier where the event was stored before',
+ * );
+ */
+
+/**
+ * Interface definition for calendar driver classes
+ */
+abstract class calendar_driver
+{
+ const FILTER_ALL = 0;
+ const FILTER_WRITEABLE = 1;
+ const FILTER_INSERTABLE = 2;
+ const FILTER_ACTIVE = 4;
+ const FILTER_PERSONAL = 8;
+ const FILTER_PRIVATE = 16;
+ const FILTER_CONFIDENTIAL = 32;
+ const BIRTHDAY_CALENDAR_ID = '__bdays__';
+
+ // features supported by backend
+ public $alarms = false;
+ public $attendees = false;
+ public $freebusy = false;
+ public $attachments = false;
+ public $undelete = false;
+ public $history = false;
+ public $categoriesimmutable = false;
+ public $alarm_types = array('DISPLAY');
+ public $alarm_absolute = true;
+ public $last_error;
+
+ protected $default_categories = array(
+ 'Personal' => 'c0c0c0',
+ 'Work' => 'ff0000',
+ 'Family' => '00ff00',
+ 'Holiday' => 'ff6600',
+ );
+
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param integer Bitmask defining filter criterias.
+ * See FILTER_* constants for possible values.
+ * @return array List of calendars
+ */
+ abstract function list_calendars($filter = 0);
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * showalarms: True if alarms are enabled
+ * @return mixed ID of the calendar on success, False on error
+ */
+ abstract function create_calendar($prop);
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @param array Hash array with calendar properties
+ * id: Calendar Identifier
+ * name: Calendar name
+ * color: The color of the calendar
+ * showalarms: True if alarms are enabled (if supported)
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function edit_calendar($prop);
+
+ /**
+ * Set active/subscribed state of a calendar
+ *
+ * @param array Hash array with calendar properties
+ * id: Calendar Identifier
+ * active: True if calendar is active, false if not
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function subscribe_calendar($prop);
+
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @param array Hash array with calendar properties
+ * id: Calendar Identifier
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function delete_calendar($prop);
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ abstract function search_calendars($query, $source);
+
+ /**
+ * Add a single event to the database
+ *
+ * @param array Hash array with event properties (see header of this file)
+ * @return mixed New event ID on success, False on error
+ */
+ abstract function new_event($event);
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @param array Hash array with event properties (see header of this file)
+ * @return boolean True on success, False on error
+ */
+ abstract function edit_event($event);
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ return $this->edit_event($event);
+ }
+
+ /**
+ * Update the participant status for the given attendee
+ *
+ * @param array Hash array with event properties
+ * @param array List of hash arrays each represeting an updated attendee
+ * @return boolean True on success, False on error
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ return $this->edit_event($event);
+ }
+
+ /**
+ * Move a single event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * start: Event start date/time as DateTime object
+ * end: Event end date/time as DateTime object
+ * allday: Boolean flag if this is an all-day event
+ * @return boolean True on success, False on error
+ */
+ abstract function move_event($event);
+
+ /**
+ * Resize a single event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * start: Event start date/time as DateTime object with timezone
+ * end: Event end date/time as DateTime object with timezone
+ * @return boolean True on success, False on error
+ */
+ abstract function resize_event($event);
+
+ /**
+ * Remove a single event from the database
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * @param boolean Remove event irreversible (mark as deleted otherwise,
+ * if supported by the backend)
+ *
+ * @return boolean True on success, False on error
+ */
+ abstract function remove_event($event, $force = true);
+
+ /**
+ * Restores a single deleted event (if supported)
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ *
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Return data of a single event
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * uid: Event UID
+ * _instance: Instance identifier in combination with uid (optional)
+ * calendar: Calendar identifier (optional)
+ * @param integer Bitmask defining the scope to search events in.
+ * See FILTER_* constants for possible values.
+ * @param boolean If true, recurrence exceptions shall be added
+ *
+ * @return array Event object as hash array
+ */
+ abstract function get_event($event, $scope = 0, $full = false);
+
+ /**
+ * Get events from source.
+ *
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @param string Search query (optional)
+ * @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
+ * @param boolean Include virtual/recurring events (optional)
+ * @param integer Only list events modified since this time (unix timestamp)
+ * @return array A list of event objects (see header of this file for struct of an event)
+ */
+ abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null);
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ abstract function count_events($calendars, $start, $end = null);
+
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @param integer Current time (unix timestamp)
+ * @param mixed List of calendar IDs to show alarms for (either as array or comma-separated string)
+ * @return array A list of alarms, each encoded as hash array:
+ * id: Event identifier
+ * uid: Unique identifier of this event
+ * start: Event start date/time as DateTime object
+ * end: Event end date/time as DateTime object
+ * allday: Boolean flag if this is an all-day event
+ * title: Event title/summary
+ * location: Location string
+ */
+ abstract function pending_alarms($time, $calendars = null);
+
+ /**
+ * (User) feedback after showing an alarm notification
+ * This should mark the alarm as 'shown' or snooze it for the given amount of time
+ *
+ * @param string Event identifier
+ * @param integer Suspend the alarm for this number of seconds
+ */
+ abstract function dismiss_alarm($event_id, $snooze = 0);
+
+ /**
+ * Check the given event object for validity
+ *
+ * @param array Event object as hash array
+ * @return boolean True if valid, false if not
+ */
+ public function validate($event)
+ {
+ $valid = true;
+
+ if (!is_object($event['start']) || !is_a($event['start'], 'DateTime'))
+ $valid = false;
+ if (!is_object($event['end']) || !is_a($event['end'], 'DateTime'))
+ $valid = false;
+
+ return $valid;
+ }
+
+
+ /**
+ * Get list of event's attachments.
+ * Drivers can return list of attachments as event property.
+ * If they will do not do this list_attachments() method will be used.
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of attachments, each as hash array:
+ * id: Attachment identifier
+ * name: Attachment name
+ * mimetype: MIME content type of the attachment
+ * size: Attachment size
+ */
+ public function list_attachments($event) { }
+
+ /**
+ * Get attachment properties
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array Hash array with attachment properties:
+ * id: Attachment identifier
+ * name: Attachment name
+ * mimetype: MIME content type of the attachment
+ * size: Attachment size
+ */
+ public function get_attachment($id, $event) { }
+
+ /**
+ * Get attachment body
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return string Attachment body
+ */
+ public function get_attachment_body($id, $event) { }
+
+ /**
+ * Build a struct representing the given message reference
+ *
+ * @param object|string $uri_or_headers rcube_message_header instance holding the message headers
+ * or an URI from a stored link referencing a mail message.
+ * @param string $folder IMAP folder the message resides in
+ *
+ * @return array An struct referencing the given IMAP message
+ */
+ public function get_message_reference($uri_or_headers, $folder = null)
+ {
+ // to be implemented by the derived classes
+ return false;
+ }
+
+ /**
+ * List availabale categories
+ * The default implementation reads them from config/user prefs
+ */
+ public function list_categories()
+ {
+ $rcmail = rcube::get_instance();
+ return $rcmail->config->get('calendar_categories', $this->default_categories);
+ }
+
+ /**
+ * Create a new category
+ */
+ public function add_category($name, $color) { }
+
+ /**
+ * Remove the given category
+ */
+ public function remove_category($name) { }
+
+ /**
+ * Update/replace a category
+ */
+ public function replace_category($oldname, $name, $color) { }
+
+ /**
+ * Fetch free/busy information from a person within the given range
+ *
+ * @param string E-mail address of attendee
+ * @param integer Requested period start date/time as unix timestamp
+ * @param integer Requested period end date/time as unix timestamp
+ *
+ * @return array List of busy timeslots within the requested range
+ */
+ public function get_freebusy_list($email, $start, $end)
+ {
+ return false;
+ }
+
+ /**
+ * Create instances of a recurring event
+ *
+ * @param array Hash array with event properties
+ * @param object DateTime Start date of the recurrence window
+ * @param object DateTime End date of the recurrence window
+ * @return array List of recurring event instances
+ */
+ public function get_recurring_events($event, $start, $end = null)
+ {
+ $events = array();
+
+ if ($event['recurrence']) {
+ // include library class
+ require_once(dirname(__FILE__) . '/../lib/calendar_recurrence.php');
+
+ $rcmail = rcmail::get_instance();
+ $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+
+ // determine a reasonable end date if none given
+ if (!$end) {
+ switch ($event['recurrence']['FREQ']) {
+ case 'YEARLY': $intvl = 'P100Y'; break;
+ case 'MONTHLY': $intvl = 'P20Y'; break;
+ default: $intvl = 'P10Y'; break;
+ }
+
+ $end = clone $event['start'];
+ $end->add(new DateInterval($intvl));
+ }
+
+ $i = 0;
+ while ($next_event = $recurrence->next_instance()) {
+ // add to output if in range
+ if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
+ $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
+ $next_event['id'] = $next_event['uid'] . '-' . $next_event['_instance'];
+ $next_event['recurrence_id'] = $event['uid'];
+ $events[] = $next_event;
+ }
+ else if ($next_event['start'] > $end) { // stop loop if out of range
+ break;
+ }
+
+ // avoid endless recursion loops
+ if (++$i > 1000) {
+ break;
+ }
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of changes, each as a hash array:
+ * rev: Revision number
+ * type: Type of the change (create, update, move, delete)
+ * date: Change date
+ * user: The user who executed the change
+ * ip: Client IP
+ * destination: Destination calendar for 'move' type
+ */
+ public function get_event_changelog($event)
+ {