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 |
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 } |