diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist index 34b10092..9a472a7a 100644 --- a/plugins/calendar/config.inc.php.dist +++ b/plugins/calendar/config.inc.php.dist @@ -31,6 +31,9 @@ $rcmail_config['calendar_driver'] = "database"; // default calendar view (agendaDay, agendaWeek, month) $rcmail_config['calendar_default_view'] = "agendaWeek"; +// show a birthdays calendar from the user's address book(s) +$rcmail_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 $rcmail_config['calendar_date_format_sets'] = array( diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index c09d8b9d..0eedf7fd 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -8,7 +8,7 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof - * Copyright (C) 2012, Kolab Systems AG + * Copyright (C) 2012-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 @@ -81,6 +81,8 @@ */ abstract class calendar_driver { + const BIRTHDAY_CALENDAR_ID = '__bdays__'; + // features supported by backend public $alarms = false; public $attendees = false; @@ -398,7 +400,120 @@ abstract class calendar_driver */ public function get_color_values() { - return false; + return false; + } + + /** + * Compose a list of birthday events from the contact records in the user's address books. + * + * This is a default implementation using Roundcube's address book API. + * It can be overriden with a more optimized version by the individual drivers. + * + * @param integer Event's new start (unix timestamp) + * @param integer Event's new end (unix timestamp) + * @param string Search query (optional) + * @return array A list of event records + */ + public function load_birthday_events($start, $end, $search = null) + { + // convert to DateTime for comparisons + $start = new DateTime('@'.$start); + $end = new DateTime('@'.$end); + // extract the current year + $year = $start->format('Y'); + $year2 = $end->format('Y'); + + $events = array(); + $search = mb_strtolower($search); + $rcmail = rcmail::get_instance(); + $cache = $rcmail->get_cache('calendar.birthdays', 'db', 3600); + $cache->expunge(); + + // TODO: let the user select the address books to consider in prefs + foreach ($rcmail->get_address_sources(false, true) as $source) { + $abook = $rcmail->get_address_book($source['id']); + $abook->set_pagesize(10000); + + // skip LDAP address books (really?) + if ($abook instanceof rcube_ldap) { + continue; + } + + // check for cached results + $cache_records = array(); + $cached = $cache->get($source['id']); + + // iterate over (cached) contacts + foreach ((array)($cached ?: $abook->list_records()) as $contact) { + if (!empty($contact['birthday'])) { + try { + if (is_array($contact['birthday'])) + $contact['birthday'] = reset($contact['birthday']); + + $bday = $contact['birthday'] instanceof DateTime ? $contact['birthday'] : + new DateTime($contact['birthday'], new DateTimezone('UTC')); + $birthyear = $bday->format('Y'); + } + catch (Exception $e) { + // console('BIRTHDAY PARSE ERROR: ' . $e); + continue; + } + + $display_name = rcube_addressbook::compose_display_name($contact); + $event_title = $rcmail->gettext(array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name)), 'calendar'); + + // add stripped record to cache + if (empty($cached)) { + $cache_records[] = array( + 'id' => $contact['ID'], + 'name' => $display_name, + 'birthday' => $bday->format('Y-m-d'), + ); + } + + // filter by search term (only name is involved here) + if (!empty($search) && strpos(mb_strtolower($event_title), $search) === false) { + continue; + } + + // quick-and-dirty recurrence computation: just replace the year + $bday->setDate($year, $bday->format('n'), $bday->format('j')); + $bday->setTime(12, 0, 0); + + // date range reaches over multiple years: use end year if not in range + if (($bday > $end || $bday < $start) && $year2 != $year) { + $bday->setDate($year2, $bday->format('n'), $bday->format('j')); + $year = $year2; + } + + // birthday is within requested range + if ($bday <= $end && $bday >= $start) { + $age = $year - $birthyear; + $event = array( + 'id' => md5('bday_' . $contact['id']), + 'calendar' => self::BIRTHDAY_CALENDAR_ID, + 'title' => $event_title, + 'description' => $rcmail->gettext(array('name' => 'birthdayage', 'vars' => array('age' => $age)), 'calendar'), + // Add more contact information to description block? + 'allday' => true, + 'start' => $bday, + // TODO: add alarms (configurable?) + ); + $event['end'] = clone $bday; + $event['end']->add(new DateInterval('PT1H')); + + $events[] = $event; + } + } + } + + // store collected contacts in cache + if (empty($cached)) { + $cache->write($source['id'], $cache_records); + } + } + + return $events; } } diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 43c2e1b0..18fed049 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -8,7 +8,7 @@ * @author Thomas Bruederli * * Copyright (C) 2010, Lazlo Westerhof - * Copyright (C) 2012, Kolab Systems AG + * Copyright (C) 2012-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 @@ -127,6 +127,28 @@ class database_driver extends calendar_driver // 'personal' is unsupported in this driver + // append the virtual birthdays calendar + if ($this->rc->config->get('calendar_contact_birthdays', false)) { + $prefs = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); + $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); + + $id = self::BIRTHDAY_CALENDAR_ID; + if (!$active || !in_array($id, $hidden)) { + $calendars[$id] = array( + 'id' => $id, + 'name' => $this->cal->gettext('birthdays'), + 'listname' => $this->cal->gettext('birthdays'), + 'color' => $prefs['color'], + 'showalarms' => $prefs['showalarms'], + 'active' => !in_array($id, $hidden), + 'class_name' => 'birthdays', + 'readonly' => true, + 'default' => false, + 'children' => false, + ); + } + } + return $calendars; } @@ -163,6 +185,17 @@ class database_driver extends calendar_driver */ public function edit_calendar($prop) { + // birthday calendar properties are saved in user prefs + if ($prop['id'] == self::BIRTHDAY_CALENDAR_ID) { + $prefs = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA')); + if (isset($prop['color'])) + $prefs['color'] = $prop['color']; + if (isset($prop['showalarms'])) + $prefs['showalarms'] = $prop['showalarms'] ? true : false; + $this->rc->user->save_prefs(array('birthday_calendar' => $prefs)); + return true; + } + $query = $this->rc->db->query( "UPDATE " . $this->db_calendars . " SET name=?, color=?, showalarms=? @@ -777,7 +810,12 @@ class database_driver extends calendar_driver $events[] = $this->_read_postprocess($event); } } - + + // add events from the address books birthday calendar + if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) { + $events = array_merge($events, $this->load_birthday_events($start, $end, $search)); + } + return $events; } diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index f673f6c3..87a5f022 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -7,7 +7,7 @@ * @author Thomas Bruederli * @author Aleksander Machniak * - * Copyright (C) 2012, Kolab Systems AG + * Copyright (C) 2012-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 @@ -145,6 +145,26 @@ class kolab_driver extends calendar_driver } } + // append the virtual birthdays calendar + if ($this->rc->config->get('calendar_contact_birthdays', false)) { + $id = self::BIRTHDAY_CALENDAR_ID; + $prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs + if (!$active || $prefs[$id]['active']) { + $calendars[$id] = array( + 'id' => $id, + 'name' => $this->cal->gettext('birthdays'), + 'listname' => $this->cal->gettext('birthdays'), + 'color' => $prefs[$id]['color'], + 'showalarms' => $prefs[$id]['showalarms'], + 'active' => $prefs[$id]['active'], + 'class_name' => 'birthdays', + 'readonly' => true, + 'default' => false, + 'children' => false, + ); + } + } + return $calendars; } @@ -245,23 +265,24 @@ class kolab_driver extends calendar_driver // create ID $id = kolab_storage::folder_id($newfolder); - - // fallback to local prefs - $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); - unset($prefs['kolab_calendars'][$prop['id']]); - - if (isset($prop['color'])) - $prefs['kolab_calendars'][$id]['color'] = $prop['color']; - if (isset($prop['showalarms'])) - $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false; - - if ($prefs['kolab_calendars'][$id]) - $this->rc->user->save_prefs($prefs); - - return true; + } + else { + $id = $prop['id']; } - return false; + // fallback to local prefs + $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); + unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']); + + if (isset($prop['color'])) + $prefs['kolab_calendars'][$id]['color'] = $prop['color']; + if (isset($prop['showalarms'])) + $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false; + + if (!empty($prefs['kolab_calendars'][$id])) + $this->rc->user->save_prefs($prefs); + + return true; } @@ -275,6 +296,13 @@ class kolab_driver extends calendar_driver if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) { return $cal->storage->activate($prop['active']); } + else { + // save state in local prefs + $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array()); + $prefs['kolab_calendars'][$prop['id']]['active'] = (bool)$prop['active']; + $this->rc->user->save_prefs($prefs); + return true; + } return false; } @@ -724,7 +752,12 @@ class kolab_driver extends calendar_driver $events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual, $query)); $categories += $this->calendars[$cid]->categories; } - + + // add events from the address books birthday calendar + if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) { + $events = array_merge($events, $this->load_birthday_events($start, $end, $search)); + } + // add new categories to user prefs $old_categories = $this->rc->config->get('calendar_categories', $this->default_categories); if ($newcats = array_diff(array_map('strtolower', array_keys($categories)), array_map('strtolower', array_keys($old_categories)))) { diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 5c41ed11..c582db92 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -238,4 +238,9 @@ $labels['futurevents'] = 'Future'; $labels['allevents'] = 'All'; $labels['saveasnew'] = 'Save as new'; +// birthdays calendar +$labels['birthdays'] = 'Birthdays'; +$labels['birthdayeventtitle'] = '$name\'s Birthday'; +$labels['birthdayage'] = 'Age $age'; + ?>