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