1<?php
2
3/**
4 * Driver interface for the Calendar plugin
5 *
6 * @version @package_version@
7 * @author Lazlo Westerhof <hello@lazlo.me>
8 * @author Thomas Bruederli <bruederli@kolabsys.com>
9 *
10 * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
11 * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
12 *
13 * This program is free software: you can redistribute it and/or modify
14 * it under the terms of the GNU Affero General Public License as
15 * published by the Free Software Foundation, either version 3 of the
16 * License, or (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU Affero General Public License for more details.
22 *
23 * You should have received a copy of the GNU Affero General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27
28/**
29 * Struct of an internal event object how it is passed from/to the driver classes:
30 *
31 *  $event = array(
32 *            'id' => 'Event ID used for editing',
33 *           'uid' => 'Unique identifier of this event',
34 *      'calendar' => 'Calendar identifier to add event to or where the event is stored',
35 *         'start' => DateTime,  // Event start date/time as DateTime object
36 *           'end' => DateTime,  // Event end date/time as DateTime object
37 *        'allday' => true|false,  // Boolean flag if this is an all-day event
38 *       'changed' => DateTime,    // Last modification date of event
39 *         'title' => 'Event title/summary',
40 *      'location' => 'Location string',
41 *   'description' => 'Event description',
42 *           'url' => 'URL to more information',
43 *    'recurrence' => array(   // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs
44 *            'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY',
45 *        'INTERVAL' => 1...n,
46 *           'UNTIL' => DateTime,
47 *           'COUNT' => 1..n,   // number of times
48 *                      // + more properties (see http://www.kanzaki.com/docs/ical/recur.html)
49 *          'EXDATE' => array(),  // list of DateTime objects of exception Dates/Times
50 *      'EXCEPTIONS' => array(<event>),  list of event objects which denote exceptions in the recurrence chain
51 *    ),
52 * 'recurrence_id' => 'ID of the recurrence group',   // usually the ID of the starting event
53 *     '_instance' => 'ID of the recurring instance',   // identifies an instance within a recurrence chain
54 *    'categories' => 'Event category',
55 *     'free_busy' => 'free|busy|outofoffice|tentative',  // Show time as
56 *        'status' => 'TENTATIVE|CONFIRMED|CANCELLED',    // event status according to RFC 2445
57 *      'priority' => 0-9,     // Event priority (0=undefined, 1=highest, 9=lowest)
58 *   'sensitivity' => 'public|private|confidential',   // Event sensitivity
59 *        'alarms' => '-15M:DISPLAY',  // DEPRECATED Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event)
60 *       'valarms' => array(           // List of reminders (new format), each represented as a hash array:
61 *                  array(
62 *                     'trigger' => '-PT90M',     // ISO 8601 period string prefixed with '+' or '-', or DateTime object
63 *                      'action' => 'DISPLAY|EMAIL|AUDIO',
64 *                    'duration' => 'PT15M',      // ISO 8601 period string
65 *                      'repeat' => 0,            // number of repetitions
66 *                 'description' => '',        // text to display for DISPLAY actions
67 *                     'summary' => '',        // message text for EMAIL actions
68 *                   'attendees' => array(),   // list of email addresses to receive alarm messages
69 *                  ),
70 *   ),
71 *   'attachments' => array(   // List of attachments
72 *            'name' => 'File name',
73 *        'mimetype' => 'Content type',
74 *            'size' => 1..n, // in bytes
75 *              'id' => 'Attachment identifier'
76 *   ),
77 * 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated
78 *     'attendees' => array(   // List of event participants
79 *            'name' => 'Participant name',
80 *           'email' => 'Participant e-mail address',  // used as identifier
81 *            'role' => 'ORGANIZER|REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR',
82 *          'status' => 'NEEDS-ACTION|UNKNOWN|ACCEPTED|TENTATIVE|DECLINED'
83 *            'rsvp' => true|false,
84 *    ),
85 *
86 *     '_savemode' => 'all|future|current|new',   // How changes on recurring event should be handled
87 *       '_notify' => true|false,  // whether to notify event attendees about changes
88 * '_fromcalendar' => 'Calendar identifier where the event was stored before',
89 *  );
90 */
91
92/**
93 * Interface definition for calendar driver classes
94 */
95abstract class calendar_driver
96{
97  const FILTER_ALL           = 0;
98  const FILTER_WRITEABLE     = 1;
99  const FILTER_INSERTABLE    = 2;
100  const FILTER_ACTIVE        = 4;
101  const FILTER_PERSONAL      = 8;
102  const FILTER_PRIVATE       = 16;
103  const FILTER_CONFIDENTIAL  = 32;
104  const FILTER_SHARED        = 64;
105  const BIRTHDAY_CALENDAR_ID = '__bdays__';
106
107  // features supported by backend
108  public $alarms = false;
109  public $attendees = false;
110  public $freebusy = false;
111  public $attachments = false;
112  public $undelete = false;
113  public $history = false;
114  public $categoriesimmutable = false;
115  public $alarm_types = array('DISPLAY');
116  public $alarm_absolute = true;
117  public $last_error;
118
119  protected $default_categories = array(
120    'Personal' => 'c0c0c0',
121    'Work'     => 'ff0000',
122    'Family'   => '00ff00',
123    'Holiday'  => 'ff6600',
124  );
125
126  /**
127   * Get a list of available calendars from this source
128   *
129   * @param integer Bitmask defining filter criterias.
130   *          See FILTER_* constants for possible values.
131   * @return array List of calendars
132   */
133  abstract function list_calendars($filter = 0);
134
135  /**
136   * Create a new calendar assigned to the current user
137   *
138   * @param array Hash array with calendar properties
139   *        name: Calendar name
140   *       color: The color of the calendar
141   *  showalarms: True if alarms are enabled
142   * @return mixed ID of the calendar on success, False on error
143   */
144  abstract function create_calendar($prop);
145
146  /**
147   * Update properties of an existing calendar
148   *
149   * @param array Hash array with calendar properties
150   *          id: Calendar Identifier
151   *        name: Calendar name
152   *       color: The color of the calendar
153   *  showalarms: True if alarms are enabled (if supported)
154   * @return boolean True on success, Fales on failure
155   */
156  abstract function edit_calendar($prop);
157
158  /**
159   * Set active/subscribed state of a calendar
160   *
161   * @param array Hash array with calendar properties
162   *          id: Calendar Identifier
163   *      active: True if calendar is active, false if not
164   * @return boolean True on success, Fales on failure
165   */
166  abstract function subscribe_calendar($prop);
167
168  /**
169   * Delete the given calendar with all its contents
170   *
171   * @param array Hash array with calendar properties
172   *      id: Calendar Identifier
173   * @return boolean True on success, Fales on failure
174   */
175  abstract function delete_calendar($prop);
176
177  /**
178   * Search for shared or otherwise not listed calendars the user has access
179   *
180   * @param string Search string
181   * @param string Section/source to search
182   * @return array List of calendars
183   */
184  abstract function search_calendars($query, $source);
185
186  /**
187   * Add a single event to the database
188   *
189   * @param array Hash array with event properties (see header of this file)
190   * @return mixed New event ID on success, False on error
191   */
192  abstract function new_event($event);
193
194  /**
195   * Update an event entry with the given data
196   *
197   * @param array Hash array with event properties (see header of this file)
198   * @return boolean True on success, False on error
199   */
200  abstract function edit_event($event);
201
202  /**
203   * Extended event editing with possible changes to the argument
204   *
205   * @param array  Hash array with event properties
206   * @param string New participant status
207   * @param array  List of hash arrays with updated attendees
208   * @return boolean True on success, False on error
209   */
210  public function edit_rsvp(&$event, $status, $attendees)
211  {
212    return $this->edit_event($event);
213  }
214
215  /**
216   * Update the participant status for the given attendee
217   *
218   * @param array  Hash array with event properties
219   * @param array  List of hash arrays each represeting an updated attendee
220   * @return boolean True on success, False on error
221   */
222  public function update_attendees(&$event, $attendees)
223  {
224    return $this->edit_event($event);
225  }
226
227  /**
228   * Move a single event
229   *
230   * @param array Hash array with event properties:
231   *      id: Event identifier
232   *   start: Event start date/time as DateTime object
233   *     end: Event end date/time as DateTime object
234   *  allday: Boolean flag if this is an all-day event
235   * @return boolean True on success, False on error
236   */
237  abstract function move_event($event);
238
239  /**
240   * Resize a single event
241   *
242   * @param array Hash array with event properties:
243   *      id: Event identifier
244   *   start: Event start date/time as DateTime object with timezone
245   *     end: Event end date/time as DateTime object with timezone
246   * @return boolean True on success, False on error
247   */
248  abstract function resize_event($event);
249
250  /**
251   * Remove a single event from the database
252   *
253   * @param array   Hash array with event properties:
254   *      id: Event identifier
255   * @param boolean Remove event irreversible (mark as deleted otherwise,
256   *                if supported by the backend)
257   *
258   * @return boolean True on success, False on error
259   */
260  abstract function remove_event($event, $force = true);
261
262  /**
263   * Restores a single deleted event (if supported)
264   *
265   * @param array Hash array with event properties:
266   *      id: Event identifier
267   *
268   * @return boolean True on success, False on error
269   */
270  public function restore_event($event)
271  {
272    return false;
273  }
274
275  /**
276   * Return data of a single event
277   *
278   * @param mixed  UID string or hash array with event properties:
279   *         id: Event identifier
280   *        uid: Event UID
281   *  _instance: Instance identifier in combination with uid (optional)
282   *   calendar: Calendar identifier (optional)
283   * @param integer Bitmask defining the scope to search events in.
284   *          See FILTER_* constants for possible values.
285   * @param boolean If true, recurrence exceptions shall be added
286   *
287   * @return array Event object as hash array
288   */
289  abstract function get_event($event, $scope = 0, $full = false);
290
291  /**
292   * Get events from source.
293   *
294   * @param  integer Date range start (unix timestamp)
295   * @param  integer Date range end (unix timestamp)
296   * @param  string  Search query (optional)
297   * @param  mixed   List of calendar IDs to load events from (either as array or comma-separated string)
298   * @param  boolean Include virtual/recurring events (optional)
299   * @param  integer Only list events modified since this time (unix timestamp)
300   * @return array A list of event objects (see header of this file for struct of an event)
301   */
302  abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null);
303
304  /**
305   * Get number of events in the given calendar
306   *
307   * @param  mixed   List of calendar IDs to count events (either as array or comma-separated string)
308   * @param  integer Date range start (unix timestamp)
309   * @param  integer Date range end (unix timestamp)
310   * @return array   Hash array with counts grouped by calendar ID
311   */
312  abstract function count_events($calendars, $start, $end = null);
313
314  /**
315   * Get a list of pending alarms to be displayed to the user
316   *
317   * @param  integer Current time (unix timestamp)
318   * @param  mixed   List of calendar IDs to show alarms for (either as array or comma-separated string)
319   * @return array A list of alarms, each encoded as hash array:
320   *         id: Event identifier
321   *        uid: Unique identifier of this event
322   *      start: Event start date/time as DateTime object
323   *        end: Event end date/time as DateTime object
324   *     allday: Boolean flag if this is an all-day event
325   *      title: Event title/summary
326   *   location: Location string
327   */
328  abstract function pending_alarms($time, $calendars = null);
329
330  /**
331   * (User) feedback after showing an alarm notification
332   * This should mark the alarm as 'shown' or snooze it for the given amount of time
333   *
334   * @param  string  Event identifier
335   * @param  integer Suspend the alarm for this number of seconds
336   */
337  abstract function dismiss_alarm($event_id, $snooze = 0);
338
339  /**
340   * Check the given event object for validity
341   *
342   * @param array Event object as hash array
343   * @return boolean True if valid, false if not
344   */
345  public function validate($event)
346  {
347    $valid = true;
348
349    if (!is_object($event['start']) || !is_a($event['start'], 'DateTime'))
350      $valid = false;
351    if (!is_object($event['end']) || !is_a($event['end'], 'DateTime'))
352      $valid = false;
353
354    return $valid;
355  }
356
357
358  /**
359   * Get list of event's attachments.
360   * Drivers can return list of attachments as event property.
361   * If they will do not do this list_attachments() method will be used.
362   *
363   * @param array $event Hash array with event properties:
364   *         id: Event identifier
365   *   calendar: Calendar identifier
366   *
367   * @return array List of attachments, each as hash array:
368   *         id: Attachment identifier
369   *       name: Attachment name
370   *   mimetype: MIME content type of the attachment
371   *       size: Attachment size
372   */
373  public function list_attachments($event) { }
374
375  /**
376   * Get attachment properties
377   *
378   * @param string $id    Attachment identifier
379   * @param array  $event Hash array with event properties:
380   *         id: Event identifier
381   *   calendar: Calendar identifier
382   *
383   * @return array Hash array with attachment properties:
384   *         id: Attachment identifier
385   *       name: Attachment name
386   *   mimetype: MIME content type of the attachment
387   *       size: Attachment size
388   */
389  public function get_attachment($id, $event) { }
390
391  /**
392   * Get attachment body
393   *
394   * @param string $id    Attachment identifier
395   * @param array  $event Hash array with event properties:
396   *         id: Event identifier
397   *   calendar: Calendar identifier
398   *
399   * @return string Attachment body
400   */
401  public function get_attachment_body($id, $event) { }
402
403  /**
404   * Build a struct representing the given message reference
405   *
406   * @param object|string $uri_or_headers rcube_message_header instance holding the message headers
407   *                         or an URI from a stored link referencing a mail message.
408   * @param string $folder  IMAP folder the message resides in
409   *
410   * @return array An struct referencing the given IMAP message
411   */
412  public function get_message_reference($uri_or_headers, $folder = null)
413  {
414      // to be implemented by the derived classes
415      return false;
416  }
417
418  /**
419   * List availabale categories
420   * The default implementation reads them from config/user prefs
421   */
422  public function list_categories()
423  {
424    $rcmail = rcube::get_instance();
425    return $rcmail->config->get('calendar_categories', $this->default_categories);
426  }
427
428  /**
429   * Create a new category
430   */
431  public function add_category($name, $color) { }
432
433  /**
434   * Remove the given category
435   */
436  public function remove_category($name) { }
437
438  /**
439   * Update/replace a category
440   */
441  public function replace_category($oldname, $name, $color) { }
442
443  /**
444   * Fetch free/busy information from a person within the given range
445   *
446   * @param string  E-mail address of attendee
447   * @param integer Requested period start date/time as unix timestamp
448   * @param integer Requested period end date/time as unix timestamp
449   *
450   * @return array  List of busy timeslots within the requested range
451   */
452  public function get_freebusy_list($email, $start, $end)
453  {
454    return false;
455  }
456
457  /**
458   * Create instances of a recurring event
459   *
460   * @param array  Hash array with event properties
461   * @param object DateTime Start date of the recurrence window
462   * @param object DateTime End date of the recurrence window
463   * @return array List of recurring event instances
464   */
465  public function get_recurring_events($event, $start, $end = null)
466  {
467    $events = array();
468
469    if ($event['recurrence']) {
470      // include library class
471      require_once(dirname(__FILE__) . '/../lib/calendar_recurrence.php');
472
473      $rcmail = rcmail::get_instance();
474      $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
475      $recurrence_id_format = libcalendaring::recurrence_id_format($event);
476
477      // determine a reasonable end date if none given
478      if (!$end) {
479        switch ($event['recurrence']['FREQ']) {
480          case 'YEARLY':  $intvl = 'P100Y'; break;
481          case 'MONTHLY': $intvl = 'P20Y';  break;
482          default:        $intvl = 'P10Y';  break;
483        }
484
485        $end = clone $event['start'];
486        $end->add(new DateInterval($intvl));
487      }
488
489      $i = 0;
490      while ($next_event = $recurrence->next_instance()) {
491        // add to output if in range
492        if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
493          $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
494          $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance'];
495          $next_event['recurrence_id'] = $event['uid'];
496          $events[] = $next_event;
497        }
498        else if ($next_event['start'] > $end) {  // stop loop if out of range
499          break;
500        }
501
502        // avoid endless recursion loops
503        if (++$i > 1000) {
504          break;
505        }
506      }
507    }
508
509    return $events;
510  }
511
512  /**
513   * Provide a list of revisions for the given event
514   *
515   * @param array  $event Hash array with event properties:
516   *         id: Event identifier
517   *   calendar: Calendar identifier
518   *
519   * @return array List of changes, each as a hash array:
520   *         rev: Revision number
521   *        type: Type of the change (create, update, move, delete)
522   *        date: Change date
523   *        user: The user who executed the change
524   *          ip: Client IP
525   * destination: Destination calendar for 'move' type
526   */
527  public function get_event_changelog($event)
528  {
529    return false;
530  }
531
532  /**
533   * Get a list of property changes beteen two revisions of an event
534   *
535   * @param array $event Hash array with event properties:
536   *         id: Event identifier
537   *   calendar: Calendar identifier
538   * @param mixed $rev1 Old Revision
539   * @param mixed $rev2 New Revision
540   *
541   * @return array List of property changes, each as a hash array:
542   *    property: Revision number
543   *         old: Old property value
544   *         new: Updated property value
545   */
546  public function get_event_diff($event, $rev1, $rev2)
547  {
548    return false;
549  }
550
551  /**
552   * Return full data of a specific revision of an event
553   *
554   * @param mixed  UID string or hash array with event properties:
555   *        id: Event identifier
556   *  calendar: Calendar identifier
557   * @param mixed  $rev Revision number
558   *
559   * @return array Event object as hash array
560   * @see self::get_event()
561   */
562  public function get_event_revison($event, $rev)
563  {
564    return false;
565  }
566
567  /**
568   * Command the backend to restore a certain revision of an event.
569   * This shall replace the current event with an older version.
570   *
571   * @param mixed  UID string or hash array with event properties:
572   *        id: Event identifier
573   *  calendar: Calendar identifier
574   * @param mixed  $rev Revision number
575   *
576   * @return boolean True on success, False on failure
577   */
578  public function restore_event_revision($event, $rev)
579  {
580    return false;
581  }
582
583
584  /**
585   * Callback function to produce driver-specific calendar create/edit form
586   *
587   * @param string Request action 'form-edit|form-new'
588   * @param array  Calendar properties (e.g. id, color)
589   * @param array  Edit form fields
590   *
591   * @return string HTML content of the form
592   */
593  public function calendar_form($action, $calendar, $formfields)
594  {
595    $table = new html_table(array('cols' => 2, 'class' => 'propform'));
596
597    foreach ($formfields as $col => $colprop) {
598      $label = !empty($colprop['label']) ? $colprop['label'] : $rcmail->gettext("$domain.$col");
599
600      $table->add('title', html::label($colprop['id'], rcube::Q($label)));
601      $table->add(null, $colprop['value']);
602    }
603
604    return $table->show();
605  }
606
607  /**
608   * Compose a list of birthday events from the contact records in the user's address books.
609   *
610   * This is a default implementation using Roundcube's address book API.
611   * It can be overriden with a more optimized version by the individual drivers.
612   *
613   * @param  integer Event's new start (unix timestamp)
614   * @param  integer Event's new end (unix timestamp)
615   * @param  string  Search query (optional)
616   * @param  integer Only list events modified since this time (unix timestamp)
617   * @return array A list of event records
618   */
619  public function load_birthday_events($start, $end, $search = null, $modifiedsince = null)
620  {
621    // ignore update requests for simplicity reasons
622    if (!empty($modifiedsince)) {
623      return array();
624    }
625
626    // convert to DateTime for comparisons
627    $start  = new DateTime('@'.$start);
628    $end    = new DateTime('@'.$end);
629    // extract the current year
630    $year   = $start->format('Y');
631    $year2  = $end->format('Y');
632
633    $events = array();
634    $search = mb_strtolower($search);
635    $rcmail = rcmail::get_instance();
636    $cache  = $rcmail->get_cache('calendar.birthdays', 'db', 3600);
637    $cache->expunge();
638
639    $alarm_type   = $rcmail->config->get('calendar_birthdays_alarm_type', '');
640    $alarm_offset = $rcmail->config->get('calendar_birthdays_alarm_offset', '-1D');
641    $alarms       = $alarm_type ? $alarm_offset . ':' . $alarm_type : null;
642
643    // let the user select the address books to consider in prefs
644    $selected_sources = $rcmail->config->get('calendar_birthday_adressbooks');
645    $sources = $selected_sources ?: array_keys($rcmail->get_address_sources(false, true));
646    foreach ($sources as $source) {
647      $abook = $rcmail->get_address_book($source);
648
649      // skip LDAP address books unless selected by the user
650      if (!$abook || ($abook instanceof rcube_ldap && empty($selected_sources))) {
651        continue;
652      }
653
654      $abook->set_pagesize(10000);
655
656      // check for cached results
657      $cache_records = array();
658      $cached = $cache->get($source);
659
660      // iterate over (cached) contacts
661      foreach (($cached ?: $abook->search('*', '', 2, true, true, array('birthday'))) as $contact) {
662        $event = self::parse_contact($contact, $source);
663
664        if (empty($event)) {
665          continue;
666        }
667
668        // add stripped record to cache
669        if (empty($cached)) {
670          $cache_records[] = array(
671            'ID'       => $contact['ID'],
672            'name'     => $event['_displayname'],
673            'birthday' => $event['start']->format('Y-m-d'),
674          );
675        }
676
677        // filter by search term (only name is involved here)
678        if (!empty($search) && strpos(mb_strtolower($event['title']), $search) === false) {
679          continue;
680        }
681
682        $bday  = clone $event['start'];
683        $byear = $bday->format('Y');
684
685        // quick-and-dirty recurrence computation: just replace the year
686        $bday->setDate($year, $bday->format('n'), $bday->format('j'));
687        $bday->setTime(12, 0, 0);
688        $this_year = $year;
689
690        // date range reaches over multiple years: use end year if not in range
691        if (($bday > $end || $bday < $start) && $year2 != $year) {
692          $bday->setDate($year2, $bday->format('n'), $bday->format('j'));
693          $this_year = $year2;
694        }
695
696        // birthday is within requested range
697        if ($bday <= $end && $bday >= $start) {
698          unset($event['_displayname']);
699          $event['alarms'] = $alarms;
700
701          // if this is not the first occurence modify event details
702          // but not when this is "all birthdays feed" request
703          if ($year2 - $year < 10 && ($age = ($this_year - $byear))) {
704            $event['description'] = $rcmail->gettext(array('name' => 'birthdayage', 'vars' => array('age' => $age)), 'calendar');
705            $event['start']       = $bday;
706            $event['end']         = clone $bday;
707            unset($event['recurrence']);
708          }
709
710          // add the main instance
711          $events[] = $event;
712        }
713      }
714
715      // store collected contacts in cache
716      if (empty($cached)) {
717        $cache->write($source, $cache_records);
718      }
719    }
720
721    return $events;
722  }
723
724  /**
725   * Get a single birthday calendar event
726   */
727  public function get_birthday_event($id)
728  {
729    // decode $id
730    list(,$source,$contact_id,$year) = explode(':', rcube_ldap::dn_decode($id));
731
732    $rcmail = rcmail::get_instance();
733
734    if (strlen($source) && $contact_id && ($abook = $rcmail->get_address_book($source))) {
735      if ($contact = $abook->get_record($contact_id, true)) {
736        return self::parse_contact($contact, $source);
737      }
738    }
739  }
740
741  /**
742   * Parse contact and create an event for its birthday
743   *
744   * @param array  $contact Contact data
745   * @param string $source  Addressbook source ID
746   *
747   * @return array Birthday event data
748   */
749  public static function parse_contact($contact, $source)
750  {
751    if (!is_array($contact)) {
752      return;
753    }
754
755    if (is_array($contact['birthday'])) {
756      $contact['birthday'] = reset($contact['birthday']);
757    }
758
759    if (empty($contact['birthday'])) {
760      return;
761    }
762
763    try {
764      $bday = $contact['birthday'];
765      if (!$bday instanceof DateTime) {
766        $bday = new DateTime($bday, new DateTimezone('UTC'));
767      }
768      $bday->_dateonly = true;
769    }
770    catch (Exception $e) {
771      rcube::raise_error(array(
772          'code' => 600, 'type' => 'php',
773          'file' => __FILE__, 'line' => __LINE__,
774          'message' => 'BIRTHDAY PARSE ERROR: ' . $e->getMessage()),
775        true, false);
776      return;
777    }
778
779    $rcmail       = rcmail::get_instance();
780    $birthyear    = $bday->format('Y');
781    $display_name = rcube_addressbook::compose_display_name($contact);
782    $label        = array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name));
783    $event_title  = $rcmail->gettext($label, 'calendar');
784    $uid          = rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $birthyear);
785
786    $event = array(
787      'id'           => $uid,
788      'uid'          => $uid,
789      'calendar'     => self::BIRTHDAY_CALENDAR_ID,
790      'title'        => $event_title,
791      'description'  => '',
792      'allday'       => true,
793      'start'        => $bday,
794      'end'          => clone $bday,
795      'recurrence'   => array('FREQ' => 'YEARLY', 'INTERVAL' => 1),
796      'free_busy'    => 'free',
797      '_displayname' => $display_name,
798    );
799
800    return $event;
801  }
802
803  /**
804   * Store alarm dismissal for birtual birthay events
805   *
806   * @param  string  Event identifier
807   * @param  integer Suspend the alarm for this number of seconds
808   */
809  public function dismiss_birthday_alarm($event_id, $snooze = 0)
810  {
811    $rcmail = rcmail::get_instance();
812    $cache  = $rcmail->get_cache('calendar.birthdayalarms', 'db', 86400 * 30);
813    $cache->remove($event_id);
814
815    // compute new notification time or disable if not snoozed
816    $notifyat = $snooze > 0 ? time() + $snooze : null;
817    $cache->set($event_id, array('snooze' => $snooze, 'notifyat' => $notifyat));
818
819    return true;
820  }
821
822  /**
823   * Handler for user_delete plugin hook
824   *
825   * @param array Hash array with hook arguments
826   * @return array Return arguments for plugin hooks
827   */
828  public function user_delete($args)
829  {
830    // TO BE OVERRIDDEN
831    return $args;
832  }
833}
834