Mercurial > hg > rc1
comparison plugins/libcalendaring/libcalendaring.php @ 4:888e774ee983
libcalendar plugin as distributed
| author | Charlie Root |
|---|---|
| date | Sat, 13 Jan 2018 08:57:56 -0500 |
| parents | |
| children | 082a19037887 |
comparison
equal
deleted
inserted
replaced
| 3:f6fe4b6ae66a | 4:888e774ee983 |
|---|---|
| 1 <?php | |
| 2 | |
| 3 /** | |
| 4 * Library providing common functions for calendaring plugins | |
| 5 * | |
| 6 * Provides utility functions for calendar-related modules such as | |
| 7 * - alarms display and dismissal | |
| 8 * - attachment handling | |
| 9 * - recurrence computation and UI elements | |
| 10 * - ical parsing and exporting | |
| 11 * - itip scheduling protocol | |
| 12 * | |
| 13 * @version @package_version@ | |
| 14 * @author Thomas Bruederli <bruederli@kolabsys.com> | |
| 15 * | |
| 16 * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com> | |
| 17 * | |
| 18 * This program is free software: you can redistribute it and/or modify | |
| 19 * it under the terms of the GNU Affero General Public License as | |
| 20 * published by the Free Software Foundation, either version 3 of the | |
| 21 * License, or (at your option) any later version. | |
| 22 * | |
| 23 * This program is distributed in the hope that it will be useful, | |
| 24 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 26 * GNU Affero General Public License for more details. | |
| 27 * | |
| 28 * You should have received a copy of the GNU Affero General Public License | |
| 29 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| 30 */ | |
| 31 | |
| 32 class libcalendaring extends rcube_plugin | |
| 33 { | |
| 34 public $rc; | |
| 35 public $timezone; | |
| 36 public $gmt_offset; | |
| 37 public $dst_active; | |
| 38 public $timezone_offset; | |
| 39 public $ical_parts = array(); | |
| 40 public $ical_message; | |
| 41 | |
| 42 public $defaults = array( | |
| 43 'calendar_date_format' => "yyyy-MM-dd", | |
| 44 'calendar_date_short' => "M-d", | |
| 45 'calendar_date_long' => "MMM d yyyy", | |
| 46 'calendar_date_agenda' => "ddd MM-dd", | |
| 47 'calendar_time_format' => "HH:mm", | |
| 48 'calendar_first_day' => 1, | |
| 49 'calendar_first_hour' => 6, | |
| 50 'calendar_date_format_sets' => array( | |
| 51 'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'), | |
| 52 'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'), | |
| 53 'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'), | |
| 54 'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'), | |
| 55 'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'), | |
| 56 'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'), | |
| 57 'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'), | |
| 58 ), | |
| 59 ); | |
| 60 | |
| 61 private static $instance; | |
| 62 | |
| 63 private $mail_ical_parser; | |
| 64 | |
| 65 /** | |
| 66 * Singleton getter to allow direct access from other plugins | |
| 67 */ | |
| 68 public static function get_instance() | |
| 69 { | |
| 70 if (!self::$instance) { | |
| 71 self::$instance = new libcalendaring(rcube::get_instance()->plugins); | |
| 72 self::$instance->init_instance(); | |
| 73 } | |
| 74 | |
| 75 return self::$instance; | |
| 76 } | |
| 77 | |
| 78 /** | |
| 79 * Initializes class properties | |
| 80 */ | |
| 81 public function init_instance() | |
| 82 { | |
| 83 $this->rc = rcube::get_instance(); | |
| 84 | |
| 85 // set user's timezone | |
| 86 try { | |
| 87 $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT')); | |
| 88 } | |
| 89 catch (Exception $e) { | |
| 90 $this->timezone = new DateTimeZone('GMT'); | |
| 91 } | |
| 92 | |
| 93 $now = new DateTime('now', $this->timezone); | |
| 94 | |
| 95 $this->gmt_offset = $now->getOffset(); | |
| 96 $this->dst_active = $now->format('I'); | |
| 97 $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active; | |
| 98 | |
| 99 $this->add_texts('localization/', false); | |
| 100 } | |
| 101 | |
| 102 /** | |
| 103 * Required plugin startup method | |
| 104 */ | |
| 105 public function init() | |
| 106 { | |
| 107 self::$instance = $this; | |
| 108 | |
| 109 $this->rc = rcube::get_instance(); | |
| 110 $this->init_instance(); | |
| 111 | |
| 112 // include client scripts and styles | |
| 113 if ($this->rc->output) { | |
| 114 // add hook to display alarms | |
| 115 $this->add_hook('refresh', array($this, 'refresh')); | |
| 116 $this->register_action('plugin.alarms', array($this, 'alarms_action')); | |
| 117 $this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group')); | |
| 118 } | |
| 119 | |
| 120 // proceed initialization in startup hook | |
| 121 $this->add_hook('startup', array($this, 'startup')); | |
| 122 } | |
| 123 | |
| 124 /** | |
| 125 * Startup hook | |
| 126 */ | |
| 127 public function startup($args) | |
| 128 { | |
| 129 if ($this->rc->output && $this->rc->output->type == 'html') { | |
| 130 $this->rc->output->set_env('libcal_settings', $this->load_settings()); | |
| 131 $this->include_script('libcalendaring.js'); | |
| 132 $this->include_stylesheet($this->local_skin_path() . '/libcal.css'); | |
| 133 | |
| 134 $this->add_label( | |
| 135 'itipaccepted', 'itiptentative', 'itipdeclined', | |
| 136 'itipdelegated', 'expandattendeegroup', 'expandattendeegroupnodata', | |
| 137 'statusorganizer', 'statusaccepted', 'statusdeclined', | |
| 138 'statusdelegated', 'statusunknown', 'statusneeds-action', | |
| 139 'statustentative', 'statuscompleted', 'statusin-process', | |
| 140 'delegatedto', 'delegatedfrom' | |
| 141 ); | |
| 142 } | |
| 143 | |
| 144 if ($args['task'] == 'mail') { | |
| 145 if ($args['action'] == 'show' || $args['action'] == 'preview') { | |
| 146 $this->add_hook('message_load', array($this, 'mail_message_load')); | |
| 147 } | |
| 148 } | |
| 149 } | |
| 150 | |
| 151 /** | |
| 152 * Load iCalendar functions | |
| 153 */ | |
| 154 public static function get_ical() | |
| 155 { | |
| 156 $self = self::get_instance(); | |
| 157 require_once __DIR__ . '/libvcalendar.php'; | |
| 158 return new libvcalendar(); | |
| 159 } | |
| 160 | |
| 161 /** | |
| 162 * Load iTip functions | |
| 163 */ | |
| 164 public static function get_itip($domain = 'libcalendaring') | |
| 165 { | |
| 166 $self = self::get_instance(); | |
| 167 require_once __DIR__ . '/lib/libcalendaring_itip.php'; | |
| 168 return new libcalendaring_itip($self, $domain); | |
| 169 } | |
| 170 | |
| 171 /** | |
| 172 * Load recurrence computation engine | |
| 173 */ | |
| 174 public static function get_recurrence() | |
| 175 { | |
| 176 $self = self::get_instance(); | |
| 177 require_once __DIR__ . '/lib/libcalendaring_recurrence.php'; | |
| 178 return new libcalendaring_recurrence($self); | |
| 179 } | |
| 180 | |
| 181 /** | |
| 182 * Shift dates into user's current timezone | |
| 183 * | |
| 184 * @param mixed Any kind of a date representation (DateTime object, string or unix timestamp) | |
| 185 * @return object DateTime object in user's timezone | |
| 186 */ | |
| 187 public function adjust_timezone($dt, $dateonly = false) | |
| 188 { | |
| 189 if (is_numeric($dt)) | |
| 190 $dt = new DateTime('@'.$dt); | |
| 191 else if (is_string($dt)) | |
| 192 $dt = rcube_utils::anytodatetime($dt); | |
| 193 | |
| 194 if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) { | |
| 195 $dt->setTimezone($this->timezone); | |
| 196 } | |
| 197 | |
| 198 return $dt; | |
| 199 } | |
| 200 | |
| 201 | |
| 202 /** | |
| 203 * | |
| 204 */ | |
| 205 public function load_settings() | |
| 206 { | |
| 207 $this->date_format_defaults(); | |
| 208 | |
| 209 $settings = array(); | |
| 210 $keys = array('date_format', 'time_format', 'date_short', 'date_long'); | |
| 211 | |
| 212 foreach ($keys as $key) { | |
| 213 $settings[$key] = (string)$this->rc->config->get('calendar_' . $key, $this->defaults['calendar_' . $key]); | |
| 214 $settings[$key] = str_replace('Y', 'y', $settings[$key]); | |
| 215 } | |
| 216 | |
| 217 $settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '—' " . $settings['date_long'] . '}'; | |
| 218 $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); | |
| 219 $settings['timezone'] = $this->timezone_offset; | |
| 220 $settings['dst'] = $this->dst_active; | |
| 221 | |
| 222 // localization | |
| 223 $settings['days'] = array( | |
| 224 $this->rc->gettext('sunday'), $this->rc->gettext('monday'), | |
| 225 $this->rc->gettext('tuesday'), $this->rc->gettext('wednesday'), | |
| 226 $this->rc->gettext('thursday'), $this->rc->gettext('friday'), | |
| 227 $this->rc->gettext('saturday') | |
| 228 ); | |
| 229 $settings['days_short'] = array( | |
| 230 $this->rc->gettext('sun'), $this->rc->gettext('mon'), | |
| 231 $this->rc->gettext('tue'), $this->rc->gettext('wed'), | |
| 232 $this->rc->gettext('thu'), $this->rc->gettext('fri'), | |
| 233 $this->rc->gettext('sat') | |
| 234 ); | |
| 235 $settings['months'] = array( | |
| 236 $this->rc->gettext('longjan'), $this->rc->gettext('longfeb'), | |
| 237 $this->rc->gettext('longmar'), $this->rc->gettext('longapr'), | |
| 238 $this->rc->gettext('longmay'), $this->rc->gettext('longjun'), | |
| 239 $this->rc->gettext('longjul'), $this->rc->gettext('longaug'), | |
| 240 $this->rc->gettext('longsep'), $this->rc->gettext('longoct'), | |
| 241 $this->rc->gettext('longnov'), $this->rc->gettext('longdec') | |
| 242 ); | |
| 243 $settings['months_short'] = array( | |
| 244 $this->rc->gettext('jan'), $this->rc->gettext('feb'), | |
| 245 $this->rc->gettext('mar'), $this->rc->gettext('apr'), | |
| 246 $this->rc->gettext('may'), $this->rc->gettext('jun'), | |
| 247 $this->rc->gettext('jul'), $this->rc->gettext('aug'), | |
| 248 $this->rc->gettext('sep'), $this->rc->gettext('oct'), | |
| 249 $this->rc->gettext('nov'), $this->rc->gettext('dec') | |
| 250 ); | |
| 251 $settings['today'] = $this->rc->gettext('today'); | |
| 252 | |
| 253 // define list of file types which can be displayed inline | |
| 254 // same as in program/steps/mail/show.inc | |
| 255 $settings['mimetypes'] = (array)$this->rc->config->get('client_mimetypes'); | |
| 256 | |
| 257 return $settings; | |
| 258 } | |
| 259 | |
| 260 | |
| 261 /** | |
| 262 * Helper function to set date/time format according to config and user preferences | |
| 263 */ | |
| 264 private function date_format_defaults() | |
| 265 { | |
| 266 static $defaults = array(); | |
| 267 | |
| 268 // nothing to be done | |
| 269 if (isset($defaults['date_format'])) | |
| 270 return; | |
| 271 | |
| 272 $defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format'))); | |
| 273 $defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format'))); | |
| 274 | |
| 275 // override defaults | |
| 276 if ($defaults['date_format']) | |
| 277 $this->defaults['calendar_date_format'] = $defaults['date_format']; | |
| 278 if ($defaults['time_format']) | |
| 279 $this->defaults['calendar_time_format'] = $defaults['time_format']; | |
| 280 | |
| 281 // derive format variants from basic date format | |
| 282 $format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']); | |
| 283 if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) { | |
| 284 $this->defaults['calendar_date_long'] = $format_set[0]; | |
| 285 $this->defaults['calendar_date_short'] = $format_set[1]; | |
| 286 $this->defaults['calendar_date_agenda'] = $format_set[2]; | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 /** | |
| 291 * Compose a date string for the given event | |
| 292 */ | |
| 293 public function event_date_text($event, $tzinfo = false) | |
| 294 { | |
| 295 $fromto = '--'; | |
| 296 | |
| 297 // handle task objects | |
| 298 if ($event['_type'] == 'task' && is_object($event['due'])) { | |
| 299 $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null; | |
| 300 $fromto = $this->rc->format_date($event['due'], $date_format, false); | |
| 301 | |
| 302 // add timezone information | |
| 303 if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) { | |
| 304 $fromto .= ' (' . strtr($tzname, '_', ' ') . ')'; | |
| 305 } | |
| 306 | |
| 307 return $fromto; | |
| 308 } | |
| 309 | |
| 310 // abort if no valid event dates are given | |
| 311 if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) { | |
| 312 return $fromto; | |
| 313 } | |
| 314 | |
| 315 $duration = $event['start']->diff($event['end'])->format('s'); | |
| 316 | |
| 317 $this->date_format_defaults(); | |
| 318 $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])); | |
| 319 $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])); | |
| 320 | |
| 321 if ($event['allday']) { | |
| 322 $fromto = $this->rc->format_date($event['start'], $date_format); | |
| 323 if (($todate = $this->rc->format_date($event['end'], $date_format)) != $fromto) | |
| 324 $fromto .= ' - ' . $todate; | |
| 325 } | |
| 326 else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) { | |
| 327 $fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) . | |
| 328 ' - ' . $this->rc->format_date($event['end'], $time_format); | |
| 329 } | |
| 330 else { | |
| 331 $fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) . | |
| 332 ' - ' . $this->rc->format_date($event['end'], $date_format) . ' ' . $this->rc->format_date($event['end'], $time_format); | |
| 333 } | |
| 334 | |
| 335 // add timezone information | |
| 336 if ($tzinfo && ($tzname = $this->timezone->getName())) { | |
| 337 $fromto .= ' (' . strtr($tzname, '_', ' ') . ')'; | |
| 338 } | |
| 339 | |
| 340 return $fromto; | |
| 341 } | |
| 342 | |
| 343 | |
| 344 /** | |
| 345 * Render HTML form for alarm configuration | |
| 346 */ | |
| 347 public function alarm_select($attrib, $alarm_types, $absolute_time = true) | |
| 348 { | |
| 349 unset($attrib['name']); | |
| 350 | |
| 351 $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3)); | |
| 352 $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10)); | |
| 353 $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6)); | |
| 354 $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id'])); | |
| 355 $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset')); | |
| 356 $select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related')); | |
| 357 $object_type = $attrib['_type'] ?: 'event'; | |
| 358 | |
| 359 $select_type->add($this->gettext('none'), ''); | |
| 360 foreach ($alarm_types as $type) | |
| 361 $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type); | |
| 362 | |
| 363 foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) | |
| 364 $select_offset->add($this->gettext('trigger' . $trigger), $trigger); | |
| 365 | |
| 366 $select_offset->add($this->gettext('trigger0'), '0'); | |
| 367 if ($absolute_time) | |
| 368 $select_offset->add($this->gettext('trigger@'), '@'); | |
| 369 | |
| 370 $select_related->add($this->gettext('relatedstart'), 'start'); | |
| 371 $select_related->add($this->gettext('relatedend' . $object_type), 'end'); | |
| 372 | |
| 373 // pre-set with default values from user settings | |
| 374 $preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); | |
| 375 $hidden = array('style' => 'display:none'); | |
| 376 $html = html::span('edit-alarm-set', | |
| 377 $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' . | |
| 378 html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'), | |
| 379 $input_value->show($preset[0]) . ' ' . | |
| 380 $select_offset->show($preset[1]) . ' ' . | |
| 381 $select_related->show() . ' ' . | |
| 382 $input_date->show('', $hidden) . ' ' . | |
| 383 $input_time->show('', $hidden) | |
| 384 ) | |
| 385 ); | |
| 386 | |
| 387 // TODO: support adding more alarms | |
| 388 #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')), | |
| 389 # $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)'); | |
| 390 | |
| 391 return $html; | |
| 392 } | |
| 393 | |
| 394 /** | |
| 395 * Get a list of email addresses of the given user (from login and identities) | |
| 396 * | |
| 397 * @param string User Email (default to current user) | |
| 398 * @return array Email addresses related to the user | |
| 399 */ | |
| 400 public function get_user_emails($user = null) | |
| 401 { | |
| 402 static $_emails = array(); | |
| 403 | |
| 404 if (empty($user)) { | |
| 405 $user = $this->rc->user->get_username(); | |
| 406 } | |
| 407 | |
| 408 // return cached result | |
| 409 if (is_array($_emails[$user])) { | |
| 410 return $_emails[$user]; | |
| 411 } | |
| 412 | |
| 413 $emails = array($user); | |
| 414 $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails)); | |
| 415 $emails = array_map('strtolower', $plugin['emails']); | |
| 416 | |
| 417 // add all emails from the current user's identities | |
| 418 if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) { | |
| 419 foreach ($this->rc->user->list_emails() as $identity) { | |
| 420 $emails[] = strtolower($identity['email']); | |
| 421 } | |
| 422 } | |
| 423 | |
| 424 $_emails[$user] = array_unique($emails); | |
| 425 return $_emails[$user]; | |
| 426 } | |
| 427 | |
| 428 /** | |
| 429 * Set the given participant status to the attendee matching the current user's identities | |
| 430 * | |
| 431 * @param array Hash array with event struct | |
| 432 * @param string The PARTSTAT value to set | |
| 433 * @return mixed Email address of the updated attendee or False if none matching found | |
| 434 */ | |
| 435 public function set_partstat(&$event, $status, $recursive = true) | |
| 436 { | |
| 437 $success = false; | |
| 438 $emails = $this->get_user_emails(); | |
| 439 foreach ((array)$event['attendees'] as $i => $attendee) { | |
| 440 if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) { | |
| 441 $event['attendees'][$i]['status'] = strtoupper($status); | |
| 442 $success = $attendee['email']; | |
| 443 } | |
| 444 } | |
| 445 | |
| 446 // apply partstat update to each existing exception | |
| 447 if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) { | |
| 448 foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) { | |
| 449 $this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false); | |
| 450 } | |
| 451 | |
| 452 // set link to top-level exceptions | |
| 453 $event['exceptions'] = &$event['recurrence']['EXCEPTIONS']; | |
| 454 } | |
| 455 | |
| 456 return $success; | |
| 457 } | |
| 458 | |
| 459 | |
| 460 /********* Alarms handling *********/ | |
| 461 | |
| 462 /** | |
| 463 * Helper function to convert alarm trigger strings | |
| 464 * into two-field values (e.g. "-45M" => 45, "-M") | |
| 465 */ | |
| 466 public static function parse_alarm_value($val) | |
| 467 { | |
| 468 if ($val[0] == '@') { | |
| 469 return array(new DateTime($val)); | |
| 470 } | |
| 471 else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) { | |
| 472 if ($m[1] == '') | |
| 473 $m[1] = '+'; | |
| 474 foreach ($m2 as $seg) { | |
| 475 $prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT'; | |
| 476 if ($seg[1] > 0) { // ignore zero values | |
| 477 // convert seconds to minutes | |
| 478 if ($seg[2] == 'S') { | |
| 479 $seg[2] = 'M'; | |
| 480 $seg[1] = max(1, round($seg[1]/60)); | |
| 481 } | |
| 482 | |
| 483 return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]); | |
| 484 } | |
| 485 } | |
| 486 | |
| 487 // return zero value nevertheless | |
| 488 return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]); | |
| 489 } | |
| 490 | |
| 491 return false; | |
| 492 } | |
| 493 | |
| 494 /** | |
| 495 * Convert the alarms list items to be processed on the client | |
| 496 */ | |
| 497 public static function to_client_alarms($valarms) | |
| 498 { | |
| 499 return array_map(function($alarm){ | |
| 500 if ($alarm['trigger'] instanceof DateTime) { | |
| 501 $alarm['trigger'] = '@' . $alarm['trigger']->format('U'); | |
| 502 } | |
| 503 else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) { | |
| 504 $alarm['trigger'] = $trigger[2]; | |
| 505 } | |
| 506 return $alarm; | |
| 507 }, (array)$valarms); | |
| 508 } | |
| 509 | |
| 510 /** | |
| 511 * Process the alarms values submitted by the client | |
| 512 */ | |
| 513 public static function from_client_alarms($valarms) | |
| 514 { | |
| 515 return array_map(function($alarm){ | |
| 516 if ($alarm['trigger'][0] == '@') { | |
| 517 try { | |
| 518 $alarm['trigger'] = new DateTime($alarm['trigger']); | |
| 519 $alarm['trigger']->setTimezone(new DateTimeZone('UTC')); | |
| 520 } | |
| 521 catch (Exception $e) { /* handle this ? */ } | |
| 522 } | |
| 523 else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) { | |
| 524 $alarm['trigger'] = $trigger[3]; | |
| 525 } | |
| 526 return $alarm; | |
| 527 }, (array)$valarms); | |
| 528 } | |
| 529 | |
| 530 /** | |
| 531 * Render localized text for alarm settings | |
| 532 */ | |
| 533 public static function alarms_text($alarms) | |
| 534 { | |
| 535 if (is_array($alarms) && is_array($alarms[0])) { | |
| 536 $texts = array(); | |
| 537 foreach ($alarms as $alarm) { | |
| 538 if ($text = self::alarm_text($alarm)) | |
| 539 $texts[] = $text; | |
| 540 } | |
| 541 | |
| 542 return join(', ', $texts); | |
| 543 } | |
| 544 else { | |
| 545 return self::alarm_text($alarms); | |
| 546 } | |
| 547 } | |
| 548 | |
| 549 /** | |
| 550 * Render localized text for a single alarm property | |
| 551 */ | |
| 552 public static function alarm_text($alarm) | |
| 553 { | |
| 554 if (is_string($alarm)) { | |
| 555 list($trigger, $action) = explode(':', $alarm); | |
| 556 } | |
| 557 else { | |
| 558 $trigger = $alarm['trigger']; | |
| 559 $action = $alarm['action']; | |
| 560 $related = $alarm['related']; | |
| 561 } | |
| 562 | |
| 563 $text = ''; | |
| 564 $rcube = rcube::get_instance(); | |
| 565 | |
| 566 switch ($action) { | |
| 567 case 'EMAIL': | |
| 568 $text = $rcube->gettext('libcalendaring.alarmemail'); | |
| 569 break; | |
| 570 case 'DISPLAY': | |
| 571 $text = $rcube->gettext('libcalendaring.alarmdisplay'); | |
| 572 break; | |
| 573 case 'AUDIO': | |
| 574 $text = $rcube->gettext('libcalendaring.alarmaudio'); | |
| 575 break; | |
| 576 } | |
| 577 | |
| 578 if ($trigger instanceof DateTime) { | |
| 579 $text .= ' ' . $rcube->gettext(array( | |
| 580 'name' => 'libcalendaring.alarmat', | |
| 581 'vars' => array('datetime' => $rcube->format_date($trigger)) | |
| 582 )); | |
| 583 } | |
| 584 else if (preg_match('/@(\d+)/', $trigger, $m)) { | |
| 585 $text .= ' ' . $rcube->gettext(array( | |
| 586 'name' => 'libcalendaring.alarmat', | |
| 587 'vars' => array('datetime' => $rcube->format_date($m[1])) | |
| 588 )); | |
| 589 } | |
| 590 else if ($val = self::parse_alarm_value($trigger)) { | |
| 591 $r = strtoupper($related ?: 'start') == 'END' ? 'end' : ''; | |
| 592 // TODO: for all-day events say 'on date of event at XX' ? | |
| 593 if ($val[0] == 0) { | |
| 594 $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r); | |
| 595 } | |
| 596 else { | |
| 597 $label = 'libcalendaring.trigger' . $r . $val[1]; | |
| 598 $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label); | |
| 599 } | |
| 600 } | |
| 601 else { | |
| 602 return false; | |
| 603 } | |
| 604 | |
| 605 return $text; | |
| 606 } | |
| 607 | |
| 608 /** | |
| 609 * Get the next alarm (time & action) for the given event | |
| 610 * | |
| 611 * @param array Record data | |
| 612 * @return array Hash array with alarm time/type or null if no alarms are configured | |
| 613 */ | |
| 614 public static function get_next_alarm($rec, $type = 'event') | |
| 615 { | |
| 616 if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED') | |
| 617 return null; | |
| 618 | |
| 619 if ($type == 'task') { | |
| 620 $timezone = self::get_instance()->timezone; | |
| 621 if ($rec['startdate']) | |
| 622 $rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone); | |
| 623 if ($rec['date']) | |
| 624 $rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone); | |
| 625 } | |
| 626 | |
| 627 if (!$rec['end']) | |
| 628 $rec['end'] = $rec['start']; | |
| 629 | |
| 630 // support legacy format | |
| 631 if (!$rec['valarms']) { | |
| 632 list($trigger, $action) = explode(':', $rec['alarms'], 2); | |
| 633 if ($alarm = self::parse_alarm_value($trigger)) { | |
| 634 $rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0])); | |
| 635 } | |
| 636 } | |
| 637 | |
| 638 $expires = new DateTime('now - 12 hours'); | |
| 639 $alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility | |
| 640 | |
| 641 // handle multiple alarms | |
| 642 $notify_at = null; | |
| 643 foreach ($rec['valarms'] as $alarm) { | |
| 644 $notify_time = null; | |
| 645 | |
| 646 if ($alarm['trigger'] instanceof DateTime) { | |
| 647 $notify_time = $alarm['trigger']; | |
| 648 } | |
| 649 else if (is_string($alarm['trigger'])) { | |
| 650 $refdate = $alarm['related'] == 'END' ? $rec['end'] : $rec['start']; | |
| 651 | |
| 652 // abort if no reference date is available to compute notification time | |
| 653 if (!is_a($refdate, 'DateTime')) | |
| 654 continue; | |
| 655 | |
| 656 // TODO: for all-day events, take start @ 00:00 as reference date ? | |
| 657 | |
| 658 try { | |
| 659 $interval = new DateInterval(trim($alarm['trigger'], '+-')); | |
| 660 $interval->invert = $alarm['trigger'][0] == '-'; | |
| 661 $notify_time = clone $refdate; | |
| 662 $notify_time->add($interval); | |
| 663 } | |
| 664 catch (Exception $e) { | |
| 665 rcube::raise_error($e, true); | |
| 666 continue; | |
| 667 } | |
| 668 } | |
| 669 | |
| 670 if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) { | |
| 671 $notify_at = $notify_time; | |
| 672 $action = $alarm['action']; | |
| 673 $alarm_prop = $alarm; | |
| 674 | |
| 675 // generate a unique alarm ID if multiple alarms are set | |
| 676 if (count($rec['valarms']) > 1) { | |
| 677 $alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis'); | |
| 678 } | |
| 679 } | |
| 680 } | |
| 681 | |
| 682 return !$notify_at ? null : array( | |
| 683 'time' => $notify_at->format('U'), | |
| 684 'action' => $action ? strtoupper($action) : 'DISPLAY', | |
| 685 'id' => $alarm_id, | |
| 686 'prop' => $alarm_prop, | |
| 687 ); | |
| 688 } | |
| 689 | |
| 690 /** | |
| 691 * Handler for keep-alive requests | |
| 692 * This will check for pending notifications and pass them to the client | |
| 693 */ | |
| 694 public function refresh($attr) | |
| 695 { | |
| 696 // collect pending alarms from all providers (e.g. calendar, tasks) | |
| 697 $plugin = $this->rc->plugins->exec_hook('pending_alarms', array( | |
| 698 'time' => time(), | |
| 699 'alarms' => array(), | |
| 700 )); | |
| 701 | |
| 702 if (!$plugin['abort'] && !empty($plugin['alarms'])) { | |
| 703 // make sure texts and env vars are available on client | |
| 704 $this->add_texts('localization/', true); | |
| 705 $this->rc->output->add_label('close'); | |
| 706 $this->rc->output->set_env('snooze_select', $this->snooze_select()); | |
| 707 $this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms'])); | |
| 708 } | |
| 709 } | |
| 710 | |
| 711 /** | |
| 712 * Handler for alarm dismiss/snooze requests | |
| 713 */ | |
| 714 public function alarms_action() | |
| 715 { | |
| 716 // $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC); | |
| 717 $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); | |
| 718 | |
| 719 $data['ids'] = explode(',', $data['id']); | |
| 720 $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data); | |
| 721 | |
| 722 if ($plugin['success']) | |
| 723 $this->rc->output->show_message('successfullysaved', 'confirmation'); | |
| 724 else | |
| 725 $this->rc->output->show_message('calendar.errorsaving', 'error'); | |
| 726 } | |
| 727 | |
| 728 /** | |
| 729 * Generate reduced and streamlined output for pending alarms | |
| 730 */ | |
| 731 private function _alarms_output($alarms) | |
| 732 { | |
| 733 $out = array(); | |
| 734 foreach ($alarms as $alarm) { | |
| 735 $out[] = array( | |
| 736 'id' => $alarm['id'], | |
| 737 'start' => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '', | |
| 738 'end' => $alarm['end'] ? $this->adjust_timezone($alarm['end'])->format('c') : '', | |
| 739 'allDay' => $alarm['allday'] == 1, | |
| 740 'action' => $alarm['action'], | |
| 741 'title' => $alarm['title'], | |
| 742 'location' => $alarm['location'], | |
| 743 'calendar' => $alarm['calendar'], | |
| 744 ); | |
| 745 } | |
| 746 | |
| 747 return $out; | |
| 748 } | |
| 749 | |
| 750 /** | |
| 751 * Render a dropdown menu to choose snooze time | |
| 752 */ | |
| 753 private function snooze_select($attrib = array()) | |
| 754 { | |
| 755 $steps = array( | |
| 756 5 => 'repeatinmin', | |
| 757 10 => 'repeatinmin', | |
| 758 15 => 'repeatinmin', | |
| 759 20 => 'repeatinmin', | |
| 760 30 => 'repeatinmin', | |
| 761 60 => 'repeatinhr', | |
| 762 120 => 'repeatinhrs', | |
| 763 1440 => 'repeattomorrow', | |
| 764 10080 => 'repeatinweek', | |
| 765 ); | |
| 766 | |
| 767 $items = array(); | |
| 768 foreach ($steps as $n => $label) { | |
| 769 $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'), | |
| 770 $this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60)))))); | |
| 771 } | |
| 772 | |
| 773 return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib); | |
| 774 } | |
| 775 | |
| 776 | |
| 777 /********* Recurrence rules handling ********/ | |
| 778 | |
| 779 /** | |
| 780 * Render localized text describing the recurrence rule of an event | |
| 781 */ | |
| 782 public function recurrence_text($rrule) | |
| 783 { | |
| 784 $limit = 10; | |
| 785 $exdates = array(); | |
| 786 $format = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']); | |
| 787 $format = self::to_php_date_format($format); | |
| 788 $format_fn = function($dt) use ($format) { | |
| 789 return rcmail::get_instance()->format_date($dt, $format); | |
| 790 }; | |
| 791 | |
| 792 if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) { | |
| 793 $exdates = array_map($format_fn, $rrule['EXDATE']); | |
| 794 } | |
| 795 | |
| 796 if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) { | |
| 797 $rdates = array_map($format_fn, $rrule['RDATE']); | |
| 798 | |
| 799 if (!empty($exdates)) { | |
| 800 $rdates = array_diff($rdates, $exdates); | |
| 801 } | |
| 802 | |
| 803 if (count($rdates) > $limit) { | |
| 804 $rdates = array_slice($rdates, 0, $limit); | |
| 805 $more = true; | |
| 806 } | |
| 807 | |
| 808 return $this->gettext('ondate') . ' ' . join(', ', $rdates) | |
| 809 . ($more ? '...' : ''); | |
| 810 } | |
| 811 | |
| 812 $output = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1); | |
| 813 | |
| 814 switch ($rrule['FREQ']) { | |
| 815 case 'DAILY': | |
| 816 $output .= $this->gettext('days'); | |
| 817 break; | |
| 818 case 'WEEKLY': | |
| 819 $output .= $this->gettext('weeks'); | |
| 820 break; | |
| 821 case 'MONTHLY': | |
| 822 $output .= $this->gettext('months'); | |
| 823 break; | |
| 824 case 'YEARLY': | |
| 825 $output .= $this->gettext('years'); | |
| 826 break; | |
| 827 } | |
| 828 | |
| 829 if ($rrule['COUNT']) { | |
| 830 $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT']))); | |
| 831 } | |
| 832 else if ($rrule['UNTIL']) { | |
| 833 $until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format); | |
| 834 } | |
| 835 else { | |
| 836 $until = $this->gettext('forever'); | |
| 837 } | |
| 838 | |
| 839 $output .= ', ' . $until; | |
| 840 | |
| 841 if (!empty($exdates)) { | |
| 842 if (count($exdates) > $limit) { | |
| 843 $exdates = array_slice($exdates, 0, $limit); | |
| 844 $more = true; | |
| 845 } | |
| 846 | |
| 847 $output = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates) | |
| 848 . ($more ? '...' : ''); | |
| 849 } | |
| 850 | |
| 851 return $output; | |
| 852 } | |
| 853 | |
| 854 /** | |
| 855 * Generate the form for recurrence settings | |
| 856 */ | |
| 857 public function recurrence_form($attrib = array()) | |
| 858 { | |
| 859 switch ($attrib['part']) { | |
| 860 // frequency selector | |
| 861 case 'frequency': | |
| 862 $select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency')); | |
| 863 $select->add($this->gettext('never'), ''); | |
| 864 $select->add($this->gettext('daily'), 'DAILY'); | |
| 865 $select->add($this->gettext('weekly'), 'WEEKLY'); | |
| 866 $select->add($this->gettext('monthly'), 'MONTHLY'); | |
| 867 $select->add($this->gettext('yearly'), 'YEARLY'); | |
| 868 $select->add($this->gettext('rdate'), 'RDATE'); | |
| 869 $html = html::label('edit-recurrence-frequency', $this->gettext('frequency')) . $select->show(''); | |
| 870 break; | |
| 871 | |
| 872 // daily recurrence | |
| 873 case 'daily': | |
| 874 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily')); | |
| 875 $html = html::div($attrib, html::label('edit-recurrence-interval-daily', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days'))); | |
| 876 break; | |
| 877 | |
| 878 // weekly recurrence form | |
| 879 case 'weekly': | |
| 880 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly')); | |
| 881 $html = html::div($attrib, html::label('edit-recurrence-interval-weekly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks'))); | |
| 882 // weekday selection | |
| 883 $daymap = array('sun','mon','tue','wed','thu','fri','sat'); | |
| 884 $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday')); | |
| 885 $first = $this->rc->config->get('calendar_first_day', 1); | |
| 886 for ($weekdays = '', $j = $first; $j <= $first+6; $j++) { | |
| 887 $d = $j % 7; | |
| 888 $weekdays .= html::label(array('class' => 'weekday'), | |
| 889 $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . | |
| 890 $this->gettext($daymap[$d]) | |
| 891 ) . ' '; | |
| 892 } | |
| 893 $html .= html::div($attrib, html::label(null, $this->gettext('bydays')) . $weekdays); | |
| 894 break; | |
| 895 | |
| 896 // monthly recurrence form | |
| 897 case 'monthly': | |
| 898 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly')); | |
| 899 $html = html::div($attrib, html::label('edit-recurrence-interval-monthly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months'))); | |
| 900 | |
| 901 $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday')); | |
| 902 for ($monthdays = '', $d = 1; $d <= 31; $d++) { | |
| 903 $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d); | |
| 904 $monthdays .= $d % 7 ? ' ' : html::br(); | |
| 905 } | |
| 906 | |
| 907 // rule selectors | |
| 908 $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode')); | |
| 909 $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable')); | |
| 910 $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each'))); | |
| 911 $table->add(null, $monthdays); | |
| 912 $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('onevery'))); | |
| 913 $table->add(null, $this->rrule_selectors($attrib['part'])); | |
| 914 | |
| 915 $html .= html::div($attrib, $table->show()); | |
| 916 break; | |
| 917 | |
| 918 // annually recurrence form | |
| 919 case 'yearly': | |
| 920 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly')); | |
| 921 $html = html::div($attrib, html::label('edit-recurrence-interval-yearly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years'))); | |
| 922 // month selector | |
| 923 $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'); | |
| 924 $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth')); | |
| 925 for ($months = '', $m = 1; $m <= 12; $m++) { | |
| 926 $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m])); | |
| 927 $months .= $m % 4 ? ' ' : html::br(); | |
| 928 } | |
| 929 $html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months); | |
| 930 | |
| 931 // day rule selection | |
| 932 $html .= html::div($attrib, html::label(null, $this->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---')); | |
| 933 break; | |
| 934 | |
| 935 // end of recurrence form | |
| 936 case 'until': | |
| 937 $radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until')); | |
| 938 $select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times')); | |
| 939 $input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10")); | |
| 940 | |
| 941 $html = html::div('line first', | |
| 942 html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' . | |
| 943 $this->gettext('forever')) | |
| 944 ); | |
| 945 | |
| 946 $forntimes = $this->gettext(array( | |
| 947 'name' => 'forntimes', | |
| 948 'vars' => array('nr' => '%s')) | |
| 949 ); | |
| 950 $html .= html::div('line', | |
| 951 $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' . | |
| 952 sprintf($forntimes, $select->show(1)) | |
| 953 ); | |
| 954 | |
| 955 $html .= html::div('line', | |
| 956 $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' . | |
| 957 $this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate'))) | |
| 958 ); | |
| 959 | |
| 960 $html = html::div($attrib, html::label(null, ucfirst($this->gettext('recurrencend'))) . $html); | |
| 961 break; | |
| 962 | |
| 963 case 'rdate': | |
| 964 $ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), ''); | |
| 965 $input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10")); | |
| 966 $button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate'))); | |
| 967 $html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show())); | |
| 968 break; | |
| 969 } | |
| 970 | |
| 971 return $html; | |
| 972 } | |
| 973 | |
| 974 /** | |
| 975 * Input field for interval selection | |
| 976 */ | |
| 977 private function interval_selector($attrib) | |
| 978 { | |
| 979 $select = new html_select($attrib); | |
| 980 $select->add(range(1,30), range(1,30)); | |
| 981 return $select; | |
| 982 } | |
| 983 | |
| 984 /** | |
| 985 * Drop-down menus for recurrence rules like "each last sunday of" | |
| 986 */ | |
| 987 private function rrule_selectors($part, $noselect = null) | |
| 988 { | |
| 989 // rule selectors | |
| 990 $select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix")); | |
| 991 if ($noselect) $select_prefix->add($noselect, ''); | |
| 992 $select_prefix->add(array( | |
| 993 $this->gettext('first'), | |
| 994 $this->gettext('second'), | |
| 995 $this->gettext('third'), | |
| 996 $this->gettext('fourth'), | |
| 997 $this->gettext('last') | |
| 998 ), | |
| 999 array(1, 2, 3, 4, -1)); | |
| 1000 | |
| 1001 $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday")); | |
| 1002 if ($noselect) $select_wday->add($noselect, ''); | |
| 1003 | |
| 1004 $daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday'); | |
| 1005 $first = $this->rc->config->get('calendar_first_day', 1); | |
| 1006 for ($j = $first; $j <= $first+6; $j++) { | |
| 1007 $d = $j % 7; | |
| 1008 $select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2))); | |
| 1009 } | |
| 1010 | |
| 1011 return $select_prefix->show() . ' ' . $select_wday->show(); | |
| 1012 } | |
| 1013 | |
| 1014 /** | |
| 1015 * Convert the recurrence settings to be processed on the client | |
| 1016 */ | |
| 1017 public function to_client_recurrence($recurrence, $allday = false) | |
| 1018 { | |
| 1019 if ($recurrence['UNTIL']) | |
| 1020 $recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c'); | |
| 1021 | |
| 1022 // format RDATE values | |
| 1023 if (is_array($recurrence['RDATE'])) { | |
| 1024 $libcal = $this; | |
| 1025 $recurrence['RDATE'] = array_map(function($rdate) use ($libcal) { | |
| 1026 return $libcal->adjust_timezone($rdate, true)->format('c'); | |
| 1027 }, $recurrence['RDATE']); | |
| 1028 } | |
| 1029 | |
| 1030 unset($recurrence['EXCEPTIONS']); | |
| 1031 | |
| 1032 return $recurrence; | |
| 1033 } | |
| 1034 | |
| 1035 /** | |
| 1036 * Process the alarms values submitted by the client | |
| 1037 */ | |
| 1038 public function from_client_recurrence($recurrence, $start = null) | |
| 1039 { | |
| 1040 if (is_array($recurrence) && !empty($recurrence['UNTIL'])) { | |
| 1041 $recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone); | |
| 1042 } | |
| 1043 | |
| 1044 if (is_array($recurrence) && is_array($recurrence['RDATE'])) { | |
| 1045 $tz = $this->timezone; | |
| 1046 $recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) { | |
| 1047 try { | |
| 1048 $dt = new DateTime($rdate, $tz); | |
| 1049 if (is_a($start, 'DateTime')) | |
| 1050 $dt->setTime($start->format('G'), $start->format('i')); | |
| 1051 return $dt; | |
| 1052 } | |
| 1053 catch (Exception $e) { | |
| 1054 return null; | |
| 1055 } | |
| 1056 }, $recurrence['RDATE']); | |
| 1057 } | |
| 1058 | |
| 1059 return $recurrence; | |
| 1060 } | |
| 1061 | |
| 1062 | |
| 1063 /********* Attachments handling *********/ | |
| 1064 | |
| 1065 /** | |
| 1066 * Handler for attachment uploads | |
| 1067 */ | |
| 1068 public function attachment_upload($session_key, $id_prefix = '') | |
| 1069 { | |
| 1070 // Upload progress update | |
| 1071 if (!empty($_GET['_progress'])) { | |
| 1072 $this->rc->upload_progress(); | |
| 1073 } | |
| 1074 | |
| 1075 $recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); | |
| 1076 $uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC); | |
| 1077 | |
| 1078 if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) { | |
| 1079 $_SESSION[$session_key] = array(); | |
| 1080 $_SESSION[$session_key]['id'] = $recid; | |
| 1081 $_SESSION[$session_key]['attachments'] = array(); | |
| 1082 } | |
| 1083 | |
| 1084 // clear all stored output properties (like scripts and env vars) | |
| 1085 $this->rc->output->reset(); | |
| 1086 | |
| 1087 if (is_array($_FILES['_attachments']['tmp_name'])) { | |
| 1088 foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) { | |
| 1089 // Process uploaded attachment if there is no error | |
| 1090 $err = $_FILES['_attachments']['error'][$i]; | |
| 1091 | |
| 1092 if (!$err) { | |
| 1093 $attachment = array( | |
| 1094 'path' => $filepath, | |
| 1095 'size' => $_FILES['_attachments']['size'][$i], | |
| 1096 'name' => $_FILES['_attachments']['name'][$i], | |
| 1097 'mimetype' => rcube_mime::file_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]), | |
| 1098 'group' => $recid, | |
| 1099 ); | |
| 1100 | |
| 1101 $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment); | |
| 1102 } | |
| 1103 | |
| 1104 if (!$err && $attachment['status'] && !$attachment['abort']) { | |
| 1105 $id = $attachment['id']; | |
| 1106 | |
| 1107 // store new attachment in session | |
| 1108 unset($attachment['status'], $attachment['abort']); | |
| 1109 $_SESSION[$session_key]['attachments'][$id] = $attachment; | |
| 1110 | |
| 1111 if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) { | |
| 1112 $button = html::img(array( | |
| 1113 'src' => $icon, | |
| 1114 'alt' => $this->rc->gettext('delete') | |
| 1115 )); | |
| 1116 } | |
| 1117 else { | |
| 1118 $button = rcube::Q($this->rc->gettext('delete')); | |
| 1119 } | |
| 1120 | |
| 1121 $content = html::a(array( | |
| 1122 'href' => "#delete", | |
| 1123 'class' => 'delete', | |
| 1124 'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", rcmail_output::JS_OBJECT_NAME, $id), | |
| 1125 'title' => $this->rc->gettext('delete'), | |
| 1126 'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'], | |
| 1127 ), $button); | |
| 1128 | |
| 1129 $content .= rcube::Q($attachment['name']); | |
| 1130 | |
| 1131 $this->rc->output->command('add2attachment_list', "rcmfile$id", array( | |
| 1132 'html' => $content, | |
| 1133 'name' => $attachment['name'], | |
| 1134 'mimetype' => $attachment['mimetype'], | |
| 1135 'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']), | |
| 1136 'complete' => true), $uploadid); | |
| 1137 } | |
| 1138 else { // upload failed | |
| 1139 if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { | |
| 1140 $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array( | |
| 1141 'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize')))))); | |
| 1142 } | |
| 1143 else if ($attachment['error']) { | |
| 1144 $msg = $attachment['error']; | |
| 1145 } | |
| 1146 else { | |
| 1147 $msg = $this->rc->gettext('fileuploaderror'); | |
| 1148 } | |
| 1149 | |
| 1150 $this->rc->output->command('display_message', $msg, 'error'); | |
| 1151 $this->rc->output->command('remove_from_attachment_list', $uploadid); | |
| 1152 } | |
| 1153 } | |
| 1154 } | |
| 1155 else if ($_SERVER['REQUEST_METHOD'] == 'POST') { | |
| 1156 // if filesize exceeds post_max_size then $_FILES array is empty, | |
| 1157 // show filesizeerror instead of fileuploaderror | |
| 1158 if ($maxsize = ini_get('post_max_size')) | |
| 1159 $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array( | |
| 1160 'size' => $this->rc->show_bytes(parse_bytes($maxsize))))); | |
| 1161 else | |
| 1162 $msg = $this->rc->gettext('fileuploaderror'); | |
| 1163 | |
| 1164 $this->rc->output->command('display_message', $msg, 'error'); | |
| 1165 $this->rc->output->command('remove_from_attachment_list', $uploadid); | |
| 1166 } | |
| 1167 | |
| 1168 $this->rc->output->send('iframe'); | |
| 1169 } | |
| 1170 | |
| 1171 | |
| 1172 /** | |
| 1173 * Deliver an event/task attachment to the client | |
| 1174 * (similar as in Roundcube core program/steps/mail/get.inc) | |
| 1175 */ | |
| 1176 public function attachment_get($attachment) | |
| 1177 { | |
| 1178 ob_end_clean(); | |
| 1179 | |
| 1180 if ($attachment && $attachment['body']) { | |
| 1181 // allow post-processing of the attachment body | |
| 1182 $part = new rcube_message_part; | |
| 1183 $part->filename = $attachment['name']; | |
| 1184 $part->size = $attachment['size']; | |
| 1185 $part->mimetype = $attachment['mimetype']; | |
| 1186 | |
| 1187 $plugin = $this->rc->plugins->exec_hook('message_part_get', array( | |
| 1188 'body' => $attachment['body'], | |
| 1189 'mimetype' => strtolower($attachment['mimetype']), | |
| 1190 'download' => !empty($_GET['_download']), | |
| 1191 'part' => $part, | |
| 1192 )); | |
| 1193 | |
| 1194 if ($plugin['abort']) | |
| 1195 exit; | |
| 1196 | |
| 1197 $mimetype = $plugin['mimetype']; | |
| 1198 list($ctype_primary, $ctype_secondary) = explode('/', $mimetype); | |
| 1199 | |
| 1200 $browser = $this->rc->output->browser; | |
| 1201 | |
| 1202 // send download headers | |
| 1203 if ($plugin['download']) { | |
| 1204 header("Content-Type: application/octet-stream"); | |
| 1205 if ($browser->ie) | |
| 1206 header("Content-Type: application/force-download"); | |
| 1207 } | |
| 1208 else if ($ctype_primary == 'text') { | |
| 1209 header("Content-Type: text/$ctype_secondary"); | |
| 1210 } | |
| 1211 else { | |
| 1212 header("Content-Type: $mimetype"); | |
| 1213 header("Content-Transfer-Encoding: binary"); | |
| 1214 } | |
| 1215 | |
| 1216 // display page, @TODO: support text/plain (and maybe some other text formats) | |
| 1217 if ($mimetype == 'text/html' && empty($_GET['_download'])) { | |
| 1218 $OUTPUT = new rcmail_html_page(); | |
| 1219 // @TODO: use washtml on $body | |
| 1220 $OUTPUT->write($plugin['body']); | |
| 1221 } | |
| 1222 else { | |
| 1223 // don't kill the connection if download takes more than 30 sec. | |
| 1224 @set_time_limit(0); | |
| 1225 | |
| 1226 $filename = $attachment['name']; | |
| 1227 $filename = preg_replace('[\r\n]', '', $filename); | |
| 1228 | |
| 1229 if ($browser->ie && $browser->ver < 7) | |
| 1230 $filename = rawurlencode(abbreviate_string($filename, 55)); | |
| 1231 else if ($browser->ie) | |
| 1232 $filename = rawurlencode($filename); | |
| 1233 else | |
| 1234 $filename = addcslashes($filename, '"'); | |
| 1235 | |
| 1236 $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline'; | |
| 1237 header("Content-Disposition: $disposition; filename=\"$filename\""); | |
| 1238 | |
| 1239 echo $plugin['body']; | |
| 1240 } | |
| 1241 | |
| 1242 exit; | |
| 1243 } | |
| 1244 | |
| 1245 // if we arrive here, the requested part was not found | |
| 1246 header('HTTP/1.1 404 Not Found'); | |
| 1247 exit; | |
| 1248 } | |
| 1249 | |
| 1250 /** | |
| 1251 * Show "loading..." page in attachment iframe | |
| 1252 */ | |
| 1253 public function attachment_loading_page() | |
| 1254 { | |
| 1255 $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']); | |
| 1256 $message = $this->rc->gettext('loadingdata'); | |
| 1257 | |
| 1258 header('Content-Type: text/html; charset=' . RCUBE_CHARSET); | |
| 1259 print "<html>\n<head>\n" | |
| 1260 . '<meta http-equiv="refresh" content="0; url='.rcube::Q($url).'">' . "\n" | |
| 1261 . '<meta http-equiv="content-type" content="text/html; charset='.RCUBE_CHARSET.'">' . "\n" | |
| 1262 . "</head>\n<body>\n$message\n</body>\n</html>"; | |
| 1263 exit; | |
| 1264 } | |
| 1265 | |
| 1266 /** | |
| 1267 * Template object for attachment display frame | |
| 1268 */ | |
| 1269 public function attachment_frame($attrib = array()) | |
| 1270 { | |
| 1271 $mimetype = strtolower($this->attachment['mimetype']); | |
| 1272 list($ctype_primary, $ctype_secondary) = explode('/', $mimetype); | |
| 1273 | |
| 1274 $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']); | |
| 1275 | |
| 1276 $this->rc->output->add_gui_object('attachmentframe', $attrib['id']); | |
| 1277 | |
| 1278 return html::iframe($attrib); | |
| 1279 } | |
| 1280 | |
| 1281 /** | |
| 1282 * | |
| 1283 */ | |
| 1284 public function attachment_header($attrib = array()) | |
| 1285 { | |
| 1286 $rcmail = rcmail::get_instance(); | |
| 1287 $dl_link = strtolower($attrib['downloadlink']) == 'true'; | |
| 1288 $dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET); | |
| 1289 | |
| 1290 $table = new html_table(array('cols' => $dl_link ? 3 : 2)); | |
| 1291 | |
| 1292 if (!empty($this->attachment['name'])) { | |
| 1293 $table->add('title', rcube::Q($this->rc->gettext('filename'))); | |
| 1294 $table->add('header', rcube::Q($this->attachment['name'])); | |
| 1295 if ($dl_link) { | |
| 1296 $table->add('download-link', html::a($dl_url, rcube::Q($this->rc->gettext('download')))); | |
| 1297 } | |
| 1298 } | |
| 1299 | |
| 1300 if (!empty($this->attachment['mimetype'])) { | |
| 1301 $table->add('title', rcube::Q($this->rc->gettext('type'))); | |
| 1302 $table->add('header', rcube::Q($this->attachment['mimetype'])); | |
| 1303 } | |
| 1304 | |
| 1305 if (!empty($this->attachment['size'])) { | |
| 1306 $table->add('title', rcube::Q($this->rc->gettext('filesize'))); | |
| 1307 $table->add('header', rcube::Q($this->rc->show_bytes($this->attachment['size']))); | |
| 1308 } | |
| 1309 | |
| 1310 $this->rc->output->set_env('attachment_download_url', $dl_url); | |
| 1311 | |
| 1312 return $table->show($attrib); | |
| 1313 } | |
| 1314 | |
| 1315 | |
| 1316 /********* iTip message detection *********/ | |
| 1317 | |
| 1318 /** | |
| 1319 * Check mail message structure of there are .ics files attached | |
| 1320 */ | |
| 1321 public function mail_message_load($p) | |
| 1322 { | |
| 1323 $this->ical_message = $p['object']; | |
| 1324 $itip_part = null; | |
| 1325 | |
| 1326 // check all message parts for .ics files | |
| 1327 foreach ((array)$this->ical_message->mime_parts as $part) { | |
| 1328 if (self::part_is_vcalendar($part, $this->ical_message)) { | |
| 1329 if ($part->ctype_parameters['method']) | |
| 1330 $itip_part = $part->mime_id; | |
| 1331 else | |
| 1332 $this->ical_parts[] = $part->mime_id; | |
| 1333 } | |
| 1334 } | |
| 1335 | |
| 1336 // priorize part with method parameter | |
| 1337 if ($itip_part) { | |
| 1338 $this->ical_parts = array($itip_part); | |
| 1339 } | |
| 1340 } | |
| 1341 | |
| 1342 /** | |
| 1343 * Getter for the parsed iCal objects attached to the current email message | |
| 1344 * | |
| 1345 * @return object libvcalendar parser instance with the parsed objects | |
| 1346 */ | |
| 1347 public function get_mail_ical_objects() | |
| 1348 { | |
| 1349 // create parser and load ical objects | |
| 1350 if (!$this->mail_ical_parser) { | |
| 1351 $this->mail_ical_parser = $this->get_ical(); | |
| 1352 | |
| 1353 foreach ($this->ical_parts as $mime_id) { | |
| 1354 $part = $this->ical_message->mime_parts[$mime_id]; | |
| 1355 $charset = $part->ctype_parameters['charset'] ?: RCUBE_CHARSET; | |
| 1356 $this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset); | |
| 1357 | |
| 1358 // check if the parsed object is an instance of a recurring event/task | |
| 1359 array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance'); | |
| 1360 | |
| 1361 // stop on the part that has an iTip method specified | |
| 1362 if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) { | |
| 1363 $this->mail_ical_parser->message_date = $this->ical_message->headers->date; | |
| 1364 $this->mail_ical_parser->mime_id = $mime_id; | |
| 1365 | |
| 1366 // store the message's sender address for comparisons | |
| 1367 $from = rcube_mime::decode_address_list($this->ical_message->headers->from, 1, true, null, true); | |
| 1368 $this->mail_ical_parser->sender = !empty($from) ? $from[1] : ''; | |
| 1369 | |
| 1370 if (!empty($this->mail_ical_parser->sender)) { | |
| 1371 foreach ($this->mail_ical_parser->objects as $i => $object) { | |
| 1372 $this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender; | |
| 1373 $this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender); | |
| 1374 } | |
| 1375 } | |
| 1376 | |
| 1377 break; | |
| 1378 } | |
| 1379 } | |
| 1380 } | |
| 1381 | |
| 1382 return $this->mail_ical_parser; | |
| 1383 } | |
| 1384 | |
| 1385 /** | |
| 1386 * Read the given mime message from IMAP and parse ical data | |
| 1387 * | |
| 1388 * @param string Mailbox name | |
| 1389 * @param string Message UID | |
| 1390 * @param string Message part ID and object index (e.g. '1.2:0') | |
| 1391 * @param string Object type filter (optional) | |
| 1392 * | |
| 1393 * @return array Hash array with the parsed iCal | |
| 1394 */ | |
| 1395 public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null) | |
| 1396 { | |
| 1397 $charset = RCUBE_CHARSET; | |
| 1398 | |
| 1399 // establish imap connection | |
| 1400 $imap = $this->rc->get_storage(); | |
| 1401 $imap->set_folder($mbox); | |
| 1402 | |
| 1403 if ($uid && $mime_id) { | |
| 1404 list($mime_id, $index) = explode(':', $mime_id); | |
| 1405 | |
| 1406 $part = $imap->get_message_part($uid, $mime_id); | |
| 1407 $headers = $imap->get_message_headers($uid); | |
| 1408 $parser = $this->get_ical(); | |
| 1409 | |
| 1410 if ($part->ctype_parameters['charset']) { | |
| 1411 $charset = $part->ctype_parameters['charset']; | |
| 1412 } | |
| 1413 | |
| 1414 if ($part) { | |
| 1415 $objects = $parser->import($part, $charset); | |
| 1416 } | |
| 1417 } | |
| 1418 | |
| 1419 // successfully parsed events/tasks? | |
| 1420 if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) { | |
| 1421 if ($parser->method) | |
| 1422 $object['_method'] = $parser->method; | |
| 1423 | |
| 1424 // store the message's sender address for comparisons | |
| 1425 $from = rcube_mime::decode_address_list($headers->from, 1, true, null, true); | |
| 1426 $object['_sender'] = !empty($from) ? $from[1] : ''; | |
| 1427 $object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']); | |
| 1428 | |
| 1429 // check if this is an instance of a recurring event/task | |
| 1430 self::identify_recurrence_instance($object); | |
| 1431 | |
| 1432 return $object; | |
| 1433 } | |
| 1434 | |
| 1435 return null; | |
| 1436 } | |
| 1437 | |
| 1438 /** | |
| 1439 * Checks if specified message part is a vcalendar data | |
| 1440 * | |
| 1441 * @param rcube_message_part Part object | |
| 1442 * @param rcube_message Message object | |
| 1443 * | |
| 1444 * @return boolean True if part is of type vcard | |
| 1445 */ | |
| 1446 public static function part_is_vcalendar($part, $message = null) | |
| 1447 { | |
| 1448 // First check if the message is "valid" (i.e. not multipart/report) | |
| 1449 if ($message) { | |
| 1450 $level = explode('.', $part->mime_id); | |
| 1451 | |
| 1452 while (array_pop($level) !== null) { | |
| 1453 $parent = $message->mime_parts[join('.', $level) ?: 0]; | |
| 1454 if ($parent->mimetype == 'multipart/report') { | |
| 1455 return false; | |
| 1456 } | |
| 1457 } | |
| 1458 } | |
| 1459 | |
| 1460 return ( | |
| 1461 in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || | |
| 1462 // Apple sends files as application/x-any (!?) | |
| 1463 ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) | |
| 1464 ); | |
| 1465 } | |
| 1466 | |
| 1467 /** | |
| 1468 * Single occourrences of recurring events are identified by their RECURRENCE-ID property | |
| 1469 * in iCal which is represented as 'recurrence_date' in our internal data structure. | |
| 1470 * | |
| 1471 * Check if such a property exists and derive the '_instance' identifier and '_savemode' | |
| 1472 * attributes which are used in the storage backend to identify the nested exception item. | |
| 1473 */ | |
| 1474 public static function identify_recurrence_instance(&$object) | |
| 1475 { | |
| 1476 // for savemode=all, remove recurrence instance identifiers | |
| 1477 if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) { | |
| 1478 unset($object['_instance'], $object['recurrence_date']); | |
| 1479 } | |
| 1480 // set instance and 'savemode' according to recurrence-id | |
| 1481 else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) { | |
| 1482 $object['_instance'] = self::recurrence_instance_identifier($object); | |
| 1483 $object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current'; | |
| 1484 } | |
| 1485 else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) { | |
| 1486 if (strlen($object['_instance']) > 4) { | |
| 1487 $object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone()); | |
| 1488 } | |
| 1489 else { | |
| 1490 $object['recurrence_date'] = clone $object['start']; | |
| 1491 } | |
| 1492 } | |
| 1493 } | |
| 1494 | |
| 1495 /** | |
| 1496 * Return a date() format string to render identifiers for recurrence instances | |
| 1497 * | |
| 1498 * @param array Hash array with event properties | |
| 1499 * @return string Format string | |
| 1500 */ | |
| 1501 public static function recurrence_id_format($event) | |
| 1502 { | |
| 1503 return $event['allday'] ? 'Ymd' : 'Ymd\THis'; | |
| 1504 } | |
| 1505 | |
| 1506 /** | |
| 1507 * Return the identifer for the given instance of a recurring event | |
| 1508 * | |
| 1509 * @param array Hash array with event properties | |
| 1510 * @param bool All-day flag from the main event | |
| 1511 * | |
| 1512 * @return mixed Format string or null if identifier cannot be generated | |
| 1513 */ | |
| 1514 public static function recurrence_instance_identifier($event, $allday = null) | |
| 1515 { | |
| 1516 $instance_date = $event['recurrence_date'] ?: $event['start']; | |
| 1517 | |
| 1518 if ($instance_date && is_a($instance_date, 'DateTime')) { | |
| 1519 // According to RFC5545 (3.8.4.4) RECURRENCE-ID format should | |
| 1520 // be date/date-time depending on the main event type, not the exception | |
| 1521 if ($allday === null) { | |
| 1522 $allday = $event['allday']; | |
| 1523 } | |
| 1524 | |
| 1525 return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis'); | |
| 1526 } | |
| 1527 } | |
| 1528 | |
| 1529 | |
| 1530 /********* Attendee handling functions *********/ | |
| 1531 | |
| 1532 /** | |
| 1533 * Handler for attendee group expansion requests | |
| 1534 */ | |
| 1535 public function expand_attendee_group() | |
| 1536 { | |
| 1537 $id = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST); | |
| 1538 $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true); | |
| 1539 $result = array('id' => $id, 'members' => array()); | |
| 1540 $maxnum = 500; | |
| 1541 | |
| 1542 // iterate over all autocomplete address books (we don't know the source of the group) | |
| 1543 foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) { | |
| 1544 if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) { | |
| 1545 foreach ($abook->list_groups($data['name'], 1) as $group) { | |
| 1546 // this is the matching group to expand | |
| 1547 if (in_array($data['email'], (array)$group['email'])) { | |
| 1548 $abook->set_pagesize($maxnum); | |
| 1549 $abook->set_group($group['ID']); | |
| 1550 | |
| 1551 // get all members | |
| 1552 $res = $abook->list_records($this->rc->config->get('contactlist_fields')); | |
| 1553 | |
| 1554 // handle errors (e.g. sizelimit, timelimit) | |
| 1555 if ($abook->get_error()) { | |
| 1556 $result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring'); | |
| 1557 $res = false; | |
| 1558 } | |
| 1559 // check for maximum number of members (we don't wanna bloat the UI too much) | |
| 1560 else if ($res->count > $maxnum) { | |
| 1561 $result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring'); | |
| 1562 $res = false; | |
| 1563 } | |
| 1564 | |
| 1565 while ($res && ($member = $res->iterate())) { | |
| 1566 $emails = (array)$abook->get_col_values('email', $member, true); | |
| 1567 if (!empty($emails) && ($email = array_shift($emails))) { | |
| 1568 $result['members'][] = array( | |
| 1569 'email' => $email, | |
| 1570 'name' => rcube_addressbook::compose_list_name($member), | |
| 1571 ); | |
| 1572 } | |
| 1573 } | |
| 1574 | |
| 1575 break 2; | |
| 1576 } | |
| 1577 } | |
| 1578 } | |
| 1579 } | |
| 1580 | |
| 1581 $this->rc->output->command('plugin.expand_attendee_callback', $result); | |
| 1582 } | |
| 1583 | |
| 1584 /** | |
| 1585 * Merge attendees of the old and new event version | |
| 1586 * with keeping current user and his delegatees status | |
| 1587 * | |
| 1588 * @param array &$new New object data | |
| 1589 * @param array $old Old object data | |
| 1590 * @param bool $status New status of the current user | |
| 1591 */ | |
| 1592 public function merge_attendees(&$new, $old, $status = null) | |
| 1593 { | |
| 1594 if (empty($status)) { | |
| 1595 $emails = $this->get_user_emails(); | |
| 1596 $delegates = array(); | |
| 1597 $attendees = array(); | |
| 1598 | |
| 1599 // keep attendee status of the current user | |
| 1600 foreach ((array) $new['attendees'] as $i => $attendee) { | |
| 1601 if (empty($attendee['email'])) { | |
| 1602 continue; | |
| 1603 } | |
| 1604 | |
| 1605 $attendees[] = $email = strtolower($attendee['email']); | |
| 1606 | |
| 1607 if (in_array($email, $emails)) { | |
| 1608 foreach ($old['attendees'] as $_attendee) { | |
| 1609 if ($attendee['email'] == $_attendee['email']) { | |
| 1610 $new['attendees'][$i] = $_attendee; | |
| 1611 if ($_attendee['status'] == 'DELEGATED' && ($email = $_attendee['delegated-to'])) { | |
| 1612 $delegates[] = strtolower($email); | |
| 1613 } | |
| 1614 | |
| 1615 break; | |
| 1616 } | |
| 1617 } | |
| 1618 } | |
| 1619 } | |
| 1620 | |
| 1621 // make sure delegated attendee is not lost | |
| 1622 foreach ($delegates as $delegatee) { | |
| 1623 if (!in_array($delegatee, $attendees)) { | |
| 1624 foreach ((array) $old['attendees'] as $attendee) { | |
| 1625 if ($attendee['email'] && ($email = strtolower($attendee['email'])) && $email == $delegatee) { | |
| 1626 $new['attendees'][] = $attendee; | |
| 1627 break; | |
| 1628 } | |
| 1629 } | |
| 1630 } | |
| 1631 } | |
| 1632 } | |
| 1633 | |
| 1634 // We also make sure that status of any attendee | |
| 1635 // is not overriden by NEEDS-ACTION if it was already set | |
| 1636 // which could happen if you work with shared events | |
| 1637 foreach ((array) $new['attendees'] as $i => $attendee) { | |
| 1638 if ($attendee['email'] && $attendee['status'] == 'NEEDS-ACTION') { | |
| 1639 foreach ($old['attendees'] as $_attendee) { | |
| 1640 if ($attendee['email'] == $_attendee['email']) { | |
| 1641 $new['attendees'][$i]['status'] = $_attendee['status']; | |
| 1642 unset($new['attendees'][$i]['rsvp']); | |
| 1643 break; | |
| 1644 } | |
| 1645 } | |
| 1646 } | |
| 1647 } | |
| 1648 } | |
| 1649 | |
| 1650 | |
| 1651 /********* Static utility functions *********/ | |
| 1652 | |
| 1653 /** | |
| 1654 * Convert the internal structured data into a vcalendar rrule 2.0 string | |
| 1655 */ | |
| 1656 public static function to_rrule($recurrence, $allday = false) | |
| 1657 { | |
| 1658 if (is_string($recurrence)) | |
| 1659 return $recurrence; | |
| 1660 | |
| 1661 $rrule = ''; | |
| 1662 foreach ((array)$recurrence as $k => $val) { | |
| 1663 $k = strtoupper($k); | |
| 1664 switch ($k) { | |
| 1665 case 'UNTIL': | |
| 1666 // convert to UTC according to RFC 5545 | |
| 1667 if (is_a($val, 'DateTime')) { | |
| 1668 if (!$allday && !$val->_dateonly) { | |
| 1669 $until = clone $val; | |
| 1670 $until->setTimezone(new DateTimeZone('UTC')); | |
| 1671 $val = $until->format('Ymd\THis\Z'); | |
| 1672 } | |
| 1673 else { | |
| 1674 $val = $val->format('Ymd'); | |
| 1675 } | |
| 1676 } | |
| 1677 break; | |
| 1678 case 'RDATE': | |
| 1679 case 'EXDATE': | |
| 1680 foreach ((array)$val as $i => $ex) { | |
| 1681 if (is_a($ex, 'DateTime')) | |
| 1682 $val[$i] = $ex->format('Ymd\THis'); | |
| 1683 } | |
| 1684 $val = join(',', (array)$val); | |
| 1685 break; | |
| 1686 case 'EXCEPTIONS': | |
| 1687 continue 2; | |
| 1688 } | |
| 1689 | |
| 1690 if (strlen($val)) | |
| 1691 $rrule .= $k . '=' . $val . ';'; | |
| 1692 } | |
| 1693 | |
| 1694 return rtrim($rrule, ';'); | |
| 1695 } | |
| 1696 | |
| 1697 /** | |
| 1698 * Convert from fullcalendar date format to PHP date() format string | |
| 1699 */ | |
| 1700 public static function to_php_date_format($from) | |
| 1701 { | |
| 1702 // "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s" | |
| 1703 return strtr(strtr($from, array( | |
| 1704 'YYYY' => 'Y', | |
| 1705 'YY' => 'y', | |
| 1706 'yyyy' => 'Y', | |
| 1707 'yy' => 'y', | |
| 1708 'MMMM' => 'F', | |
| 1709 'MMM' => 'M', | |
| 1710 'MM' => 'm', | |
| 1711 'M' => 'n', | |
| 1712 'dddd' => 'l', | |
| 1713 'ddd' => 'D', | |
| 1714 'dd' => 'd', | |
| 1715 'd' => 'j', | |
| 1716 'HH' => '**', | |
| 1717 'hh' => '%%', | |
| 1718 'H' => 'G', | |
| 1719 'h' => 'g', | |
| 1720 'mm' => 'i', | |
| 1721 'ss' => 's', | |
| 1722 'TT' => 'A', | |
| 1723 'tt' => 'a', | |
| 1724 'T' => 'A', | |
| 1725 't' => 'a', | |
| 1726 'u' => 'c', | |
| 1727 )), array( | |
| 1728 '**' => 'H', | |
| 1729 '%%' => 'h', | |
| 1730 )); | |
| 1731 } | |
| 1732 | |
| 1733 /** | |
| 1734 * Convert from PHP date() format to fullcalendar format string | |
| 1735 */ | |
| 1736 public static function from_php_date_format($from) | |
| 1737 { | |
| 1738 // "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss" | |
| 1739 return strtr($from, array( | |
| 1740 'y' => 'yy', | |
| 1741 'Y' => 'yyyy', | |
| 1742 'M' => 'MMM', | |
| 1743 'F' => 'MMMM', | |
| 1744 'm' => 'MM', | |
| 1745 'n' => 'M', | |
| 1746 'j' => 'd', | |
| 1747 'd' => 'dd', | |
| 1748 'D' => 'ddd', | |
| 1749 'l' => 'dddd', | |
| 1750 'H' => 'HH', | |
| 1751 'h' => 'hh', | |
| 1752 'G' => 'H', | |
| 1753 'g' => 'h', | |
| 1754 'i' => 'mm', | |
| 1755 's' => 'ss', | |
| 1756 'A' => 'TT', | |
| 1757 'a' => 'tt', | |
| 1758 'c' => 'u', | |
| 1759 )); | |
| 1760 } | |
| 1761 | |
| 1762 } |
