1<?php
2/**
3 * User Interface class for the Calendar plugin
4 *
5 * @author Lazlo Westerhof <hello@lazlo.me>
6 * @author Thomas Bruederli <bruederli@kolabsys.com>
7 *
8 * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
9 * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Affero General Public License as
13 * published by the Free Software Foundation, either version 3 of the
14 * License, or (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU Affero General Public License for more details.
20 *
21 * You should have received a copy of the GNU Affero General Public License
22 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23 */
24
25
26class calendar_ui
27{
28  private $rc;
29  private $cal;
30  private $ready = false;
31  public $screen;
32
33  function __construct($cal)
34  {
35    $this->cal = $cal;
36    $this->rc = $cal->rc;
37    $this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other';
38  }
39
40  /**
41   * Calendar UI initialization and requests handlers
42   */
43  public function init()
44  {
45    if ($this->ready)  // already done
46      return;
47
48    // add taskbar button
49    $this->cal->add_button(array(
50      'command'    => 'calendar',
51      'class'      => 'button-calendar',
52      'classsel'   => 'button-calendar button-selected',
53      'innerclass' => 'button-inner',
54      'label'      => 'calendar.calendar',
55      'type'       => 'link'
56      ), 'taskbar');
57
58    // load basic client script
59    if ($this->rc->action != 'print') {
60      $this->cal->include_script('calendar_base.js');
61    }
62
63    $this->addCSS();
64
65    $this->ready = true;
66  }
67
68  /**
69   * Register handler methods for the template engine
70   */
71  public function init_templates()
72  {
73    $this->cal->register_handler('plugin.calendar_css', array($this, 'calendar_css'));
74    $this->cal->register_handler('plugin.calendar_list', array($this, 'calendar_list'));
75    $this->cal->register_handler('plugin.calendar_select', array($this, 'calendar_select'));
76    $this->cal->register_handler('plugin.identity_select', array($this, 'identity_select'));
77    $this->cal->register_handler('plugin.category_select', array($this, 'category_select'));
78    $this->cal->register_handler('plugin.status_select', array($this, 'status_select'));
79    $this->cal->register_handler('plugin.freebusy_select', array($this, 'freebusy_select'));
80    $this->cal->register_handler('plugin.priority_select', array($this, 'priority_select'));
81    $this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select'));
82    $this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
83    $this->cal->register_handler('plugin.recurrence_form', array($this->cal->lib, 'recurrence_form'));
84    $this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
85    $this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
86    $this->cal->register_handler('plugin.resources_form', array($this, 'resources_form'));
87    $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list'));
88    $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form'));
89    $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info'));
90    $this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar'));
91    $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
92    $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
93    $this->cal->register_handler('plugin.edit_recurrence_sync', array($this, 'edit_recurrence_sync'));
94    $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
95    $this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
96    $this->cal->register_handler('plugin.agenda_options', array($this, 'agenda_options'));
97    $this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
98    $this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
99    $this->cal->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
100    $this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));  // use generic method from rcube_template
101
102    kolab_attachments_handler::ui();
103  }
104
105  /**
106   * Adds CSS stylesheets to the page header
107   */
108  public function addCSS()
109  {
110    $skin_path = $this->cal->local_skin_path();
111
112    if ($this->rc->task == 'calendar' && (!$this->rc->action || in_array($this->rc->action, array('index', 'print')))) {
113      // Include fullCalendar style before skin file for simpler style overriding
114      $this->cal->include_stylesheet($skin_path . '/fullcalendar.css');
115    }
116
117    $this->cal->include_stylesheet($skin_path . '/calendar.css');
118
119    if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
120      $this->cal->include_stylesheet($skin_path . '/print.css');
121    }
122  }
123
124  /**
125   * Adds JS files to the page header
126   */
127  public function addJS()
128  {
129    $this->cal->include_script('lib/js/moment.js');
130    $this->cal->include_script('lib/js/fullcalendar.js');
131
132    if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
133      $this->cal->include_script('print.js');
134    }
135    else {
136      $this->rc->output->include_script('treelist.js');
137      $this->cal->api->include_script('libkolab/libkolab.js');
138      $this->cal->include_script('calendar_ui.js');
139      jqueryui::miniColors();
140    }
141  }
142
143  /**
144   *
145   */
146  function calendar_css($attrib = array())
147  {
148    $categories    = $this->cal->driver->list_categories();
149    $js_categories = array();
150    $mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
151    $css  = "\n";
152
153    foreach ((array)$categories as $class => $color) {
154      if (!empty($color)) {
155        $js_categories[$class] = $color;
156
157        $color = ltrim($color, '#');
158        $class = 'cat-' . asciiwords(strtolower($class), true);
159        $css  .= ".$class { color: #$color; }\n";
160      }
161    }
162
163    $this->rc->output->set_env('calendar_categories', $js_categories);
164
165    $calendars = $this->cal->driver->list_calendars();
166    foreach ((array)$calendars as $id => $prop) {
167      if ($prop['color']) {
168        $css .= $this->calendar_css_classes($id, $prop, $mode, $attrib);
169      }
170    }
171
172    return html::tag('style', array('type' => 'text/css'), $css);
173  }
174
175  /**
176   *
177   */
178  public function calendar_css_classes($id, $prop, $mode, $attrib = array())
179  {
180    $color = $folder_color = $prop['color'];
181
182    // replace white with skin-defined color
183    if (!empty($attrib['folder-fallback-color']) && preg_match('/^f+$/i', $folder_color)) {
184        $folder_color = ltrim($attrib['folder-fallback-color'], '#');
185    }
186
187    $class = 'cal-' . asciiwords($id, true);
188    $css   = str_replace('$class', $class, $attrib['folder-class']) ?: "li .$class";
189    $css  .= " { color: #$folder_color; }\n";
190
191    return $css . ".$class .handle { background-color: #$color; }\n";
192  }
193
194  /**
195   *
196   */
197  function calendar_list($attrib = array(), $js_only = false)
198  {
199    $html      = '';
200    $jsenv     = array();
201    $tree      = true;
202    $calendars = $this->cal->driver->list_calendars(0, $tree);
203
204    // walk folder tree
205    if (is_object($tree)) {
206      $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
207
208      // append birthdays calendar which isn't part of $tree
209      if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) {
210        $calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal);
211      }
212      else {
213        $calendars = array();  // clear array for flat listing
214      }
215    }
216    else {
217      // fall-back to flat folder listing
218      $attrib['class'] .= ' flat';
219    }
220
221    foreach ((array)$calendars as $id => $prop) {
222      if ($attrib['activeonly'] && !$prop['active'])
223        continue;
224
225      $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
226        $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
227      );
228    }
229
230    $this->rc->output->set_env('calendars', $jsenv);
231
232    if ($js_only) {
233      return;
234    }
235
236    $this->rc->output->set_env('source', rcube_utils::get_input_value('source', rcube_utils::INPUT_GET));
237    $this->rc->output->add_gui_object('calendarslist', $attrib['id'] ?: 'unknown');
238
239    return html::tag('ul', $attrib, $html, html::$common_attrib);
240  }
241
242  /**
243   * Return html for a structured list <ul> for the folder tree
244   */
245  public function list_tree_html($node, $data, &$jsenv, $attrib)
246  {
247    $out = '';
248    foreach ($node->children as $folder) {
249      $id = $folder->id;
250      $prop = $data[$id];
251      $is_collapsed = false; // TODO: determine this somehow?
252
253      $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']);
254
255      if (!empty($folder->children)) {
256        $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
257          $this->list_tree_html($folder, $data, $jsenv, $attrib));
258      }
259
260      if (strlen($content)) {
261        $out .= html::tag('li', array(
262            'id' => 'rcmlical' . rcube_utils::html_identifier($id),
263            'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
264          ),
265          $content);
266      }
267    }
268
269    return $out;
270  }
271
272  /**
273   * Helper method to build a calendar list item (HTML content and js data)
274   */
275  public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
276  {
277    // enrich calendar properties with settings from the driver
278    if (!$prop['virtual']) {
279      unset($prop['user_id']);
280      $prop['alarms']      = $this->cal->driver->alarms;
281      $prop['attendees']   = $this->cal->driver->attendees;
282      $prop['freebusy']    = $this->cal->driver->freebusy;
283      $prop['attachments'] = $this->cal->driver->attachments;
284      $prop['undelete']    = $this->cal->driver->undelete;
285      $prop['feedurl']     = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
286
287      $jsenv[$id] = $prop;
288    }
289
290    $classes = array('calendar', 'cal-'  . asciiwords($id, true));
291    $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
292      html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : '');
293
294    if ($prop['virtual'])
295      $classes[] = 'virtual';
296    else if (!$prop['editable'])
297      $classes[] = 'readonly';
298    if ($prop['subscribed'])
299      $classes[] = 'subscribed';
300    if ($prop['subscribed'] === 2)
301      $classes[] = 'partial';
302    if ($prop['class'])
303      $classes[] = $prop['class'];
304
305    $content = '';
306    if (!$activeonly || $prop['active']) {
307      $label_id = 'cl:' . $id;
308      $content = html::div(join(' ', $classes),
309        html::a(array('class' => 'calname', 'id' => $label_id, 'title' => $title, 'href' => '#'), rcube::Q($prop['editname'] ?: $prop['listname']))
310        . ($prop['virtual'] ? '' :
311          html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) .
312          html::span('actions',
313            ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->cal->gettext('removelist')), ' ') : '') .
314            html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->cal->gettext('quickview'), 'role' => 'checkbox', 'aria-checked' => 'false'), '') .
315            (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '')
316          ) .
317          html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), '&nbsp;')
318        )
319      );
320    }
321
322    return $content;
323  }
324
325  /**
326   * Render a HTML for agenda options form
327   */
328  function agenda_options($attrib = array())
329  {
330    $attrib += array('id' => 'agendaoptions');
331    $attrib['style'] .= 'display:none';
332
333    $select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange', 'class' => 'form-control custom-select'));
334    $select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), $days);
335    foreach (array(2,5,7,14,30,60,90,180,365) as $days)
336      $select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
337
338    $html = html::span('input-group',
339        html::label(array('for' => 'agenda-listrange', 'class' => 'input-group-prepend'),
340            html::span('input-group-text', $this->cal->gettext('listrange')))
341        . $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']))
342    );
343
344    return html::div($attrib, $html);
345  }
346
347  /**
348   * Render a HTML select box for calendar selection
349   */
350  function calendar_select($attrib = array())
351  {
352    $attrib['name']       = 'calendar';
353    $attrib['is_escaped'] = true;
354    $select = new html_select($attrib);
355
356    foreach ((array)$this->cal->driver->list_calendars() as $id => $prop) {
357      if ($prop['editable'] || strpos($prop['rights'], 'i') !== false)
358        $select->add($prop['name'], $id);
359    }
360
361    return $select->show(null);
362  }
363
364  /**
365   * Render a HTML select box for user identity selection
366   */
367  function identity_select($attrib = array())
368  {
369    $attrib['name'] = 'identity';
370    $select         = new html_select($attrib);
371    $identities     = $this->rc->user->list_emails();
372
373    foreach ($identities as $ident) {
374        $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
375    }
376
377    return $select->show(null);
378  }
379
380  /**
381   * Render a HTML select box to select an event category
382   */
383  function category_select($attrib = array())
384  {
385    $attrib['name'] = 'categories';
386    $select = new html_select($attrib);
387    $select->add('---', '');
388    foreach (array_keys((array)$this->cal->driver->list_categories()) as $cat) {
389      $select->add($cat, $cat);
390    }
391
392    return $select->show(null);
393  }
394
395  /**
396   * Render a HTML select box for status property
397   */
398  function status_select($attrib = array())
399  {
400    $attrib['name'] = 'status';
401    $select = new html_select($attrib);
402    $select->add('---', '');
403    $select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED');
404    $select->add($this->cal->gettext('status-cancelled'), 'CANCELLED');
405    $select->add($this->cal->gettext('status-tentative'), 'TENTATIVE');
406    return $select->show(null);
407  }
408
409  /**
410   * Render a HTML select box for free/busy/out-of-office property
411   */
412  function freebusy_select($attrib = array())
413  {
414    $attrib['name'] = 'freebusy';
415    $select = new html_select($attrib);
416    $select->add($this->cal->gettext('free'), 'free');
417    $select->add($this->cal->gettext('busy'), 'busy');
418    // out-of-office is not supported by libkolabxml (#3220)
419    // $select->add($this->cal->gettext('outofoffice'), 'outofoffice');
420    $select->add($this->cal->gettext('tentative'), 'tentative');
421    return $select->show(null);
422  }
423
424  /**
425   * Render a HTML select for event priorities
426   */
427  function priority_select($attrib = array())
428  {
429    $attrib['name'] = 'priority';
430    $select = new html_select($attrib);
431    $select->add('---', '0');
432    $select->add('1 '.$this->cal->gettext('highest'), '1');
433    $select->add('2 '.$this->cal->gettext('high'),    '2');
434    $select->add('3 ',                                '3');
435    $select->add('4 ',                                '4');
436    $select->add('5 '.$this->cal->gettext('normal'),  '5');
437    $select->add('6 ',                                '6');
438    $select->add('7 ',                                '7');
439    $select->add('8 '.$this->cal->gettext('low'),     '8');
440    $select->add('9 '.$this->cal->gettext('lowest'),  '9');
441    return $select->show(null);
442  }
443
444  /**
445   * Render HTML input for sensitivity selection
446   */
447  function sensitivity_select($attrib = array())
448  {
449    $attrib['name'] = 'sensitivity';
450    $select = new html_select($attrib);
451    $select->add($this->cal->gettext('public'), 'public');
452    $select->add($this->cal->gettext('private'), 'private');
453    $select->add($this->cal->gettext('confidential'), 'confidential');
454    return $select->show(null);
455  }
456
457  /**
458   * Render HTML form for alarm configuration
459   */
460  function alarm_select($attrib = array())
461  {
462    return $this->cal->lib->alarm_select($attrib, $this->cal->driver->alarm_types, $this->cal->driver->alarm_absolute);
463  }
464
465  /**
466   * Render HTML for attendee notification warning
467   */
468  function edit_attendees_notify($attrib = array())
469  {
470    $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1, 'class' => 'pretty-checkbox'));
471    return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
472  }
473
474  /**
475   * Render HTML for recurrence option to align start date with the recurrence rule
476   */
477  function edit_recurrence_sync($attrib = array())
478  {
479    $checkbox = new html_checkbox(array('name' => '_start_sync', 'value' => 1, 'class' => 'pretty-checkbox'));
480    return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('eventstartsync')));
481  }
482
483  /**
484   * Generate the form for recurrence settings
485   */
486  function recurring_event_warning($attrib = array())
487  {
488    $attrib['id'] = 'edit-recurring-warning';
489
490    $radio = new html_radiobutton(array('name' => '_savemode', 'class' => 'edit-recurring-savemode'));
491    $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' .
492       html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' .
493       html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' .
494       html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew'));
495
496    return html::div($attrib, html::div('message', $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form));
497  }
498
499  /**
500   * Form for uploading and importing events
501   */
502  function events_import_form($attrib = array())
503  {
504    if (!$attrib['id'])
505      $attrib['id'] = 'rcmImportForm';
506
507    // Get max filesize, enable upload progress bar
508    $max_filesize = $this->rc->upload_init();
509
510    $accept = '.ics, text/calendar, text/x-vcalendar, application/ics';
511    if (class_exists('ZipArchive', false)) {
512      $accept .= ', .zip, application/zip';
513    }
514
515    $input = new html_inputfield(array(
516        'id'     => 'importfile',
517        'type'   => 'file',
518        'name'   => '_data',
519        'size'   => $attrib['uploadfieldsize'],
520        'accept' => $accept
521    ));
522
523    $select = new html_select(array('name' => '_range', 'id' => 'event-import-range'));
524    $select->add(array(
525        $this->cal->gettext('onemonthback'),
526        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
527        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
528        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
529        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
530        $this->cal->gettext('all'),
531      ),
532      array('1','2','3','6','12',0));
533
534    $html = html::div('form-section form-group row',
535      html::label(array('class' => 'col-sm-4 col-form-label', 'for' => 'importfile'), rcube::Q($this->rc->gettext('importfromfile')))
536      . html::div('col-sm-8', $input->show()
537        . html::div('hint', $this->rc->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))))
538    );
539
540    $html .= html::div('form-section form-group row',
541      html::label(array('for' => 'event-import-calendar', 'class' => 'col-form-label col-sm-4'), $this->cal->gettext('calendar'))
542      . html::div('col-sm-8', $this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar')))
543    );
544
545    $html .= html::div('form-section form-group row',
546      html::label(array('for' => 'event-import-range', 'class' => 'col-form-label col-sm-4'), $this->cal->gettext('importrange'))
547      . html::div('col-sm-8', $select->show(1))
548    );
549
550    $this->rc->output->add_gui_object('importform', $attrib['id']);
551    $this->rc->output->add_label('import');
552
553    return html::tag('p', null, $this->cal->gettext('importtext'))
554      . html::tag('form', array(
555          'action'  => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')),
556          'method'  => 'post',
557          'enctype' => 'multipart/form-data',
558          'id'      => $attrib['id']
559        ), $html);
560  }
561
562  /**
563   * Form to select options for exporting events
564   */
565  function events_export_form($attrib = array())
566  {
567    if (!$attrib['id'])
568      $attrib['id'] = 'rcmExportForm';
569
570    $html = html::div('form-section form-group row',
571      html::label(array('for' => 'event-export-calendar', 'class' => 'col-sm-4 col-form-label'), $this->cal->gettext('calendar'))
572        . html::div('col-sm-8', $this->calendar_select(array('name' => 'calendar', 'id' => 'event-export-calendar', 'class' => 'form-control custom-select'))));
573
574    $select = new html_select(array('name' => 'range', 'id' => 'event-export-range', 'class' => 'form-control custom-select rounded-right'));
575    $select->add(array(
576        $this->cal->gettext('all'),
577        $this->cal->gettext('onemonthback'),
578        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
579        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
580        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
581        $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
582        $this->cal->gettext('customdate'),
583      ),
584      array(0,'1','2','3','6','12','custom'));
585
586    $startdate = new html_inputfield(array('name' => 'start', 'size' => 11, 'id' => 'event-export-startdate', 'style' => 'display:none'));
587
588    $html .= html::div('form-section form-group row',
589      html::label(array('for' => 'event-export-range', 'class' => 'col-sm-4 col-form-label'), $this->cal->gettext('exportrange'))
590        . html::div('col-sm-8 input-group', $select->show(0) . $startdate->show()));
591
592    $checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'event-export-attachments', 'value' => 1, 'class' => 'form-check-input pretty-checkbox'));
593    $html .= html::div('form-section form-check row',
594      html::label(array('for' => 'event-export-attachments', 'class' => 'col-sm-4 col-form-label'), $this->cal->gettext('exportattachments'))
595        . html::div('col-sm-8', $checkbox->show(1)));
596
597    $this->rc->output->add_gui_object('exportform', $attrib['id']);
598
599    return html::tag('form', $attrib + array(
600        'action' => $this->rc->url(array('task' => 'calendar', 'action' => 'export_events')),
601        'method' => "post",
602        'id' => $attrib['id']
603      ),
604      $html
605    );
606  }
607
608  /**
609   * Handler for calendar form template.
610   * The form content could be overriden by the driver
611   */
612  function calendar_editform($action, $calendar = array())
613  {
614    $this->action   = $action;
615    $this->calendar = $calendar;
616
617    // load miniColors js/css files
618    jqueryui::miniColors();
619
620    $this->rc->output->set_env('pagetitle', $this->cal->gettext('calendarprops'));
621    $this->rc->output->add_handler('folderform', array($this, 'calendarform'));
622    $this->rc->output->send('libkolab.folderform');
623  }
624
625  /**
626   * Handler for calendar form template.
627   * The form content could be overriden by the driver
628   */
629  function calendarform($attrib)
630  {
631    // compose default calendar form fields
632    $input_name  = new html_inputfield(array('name' => 'name', 'id' => 'calendar-name', 'size' => 20));
633    $input_color = new html_inputfield(array('name' => 'color', 'id' => 'calendar-color', 'size' => 7, 'class' => 'colors'));
634
635    $formfields = array(
636      'name' => array(
637        'label' => $this->cal->gettext('name'),
638        'value' => $input_name->show($calendar['name']),
639        'id'    => 'calendar-name',
640      ),
641      'color' => array(
642        'label' => $this->cal->gettext('color'),
643        'value' => $input_color->show($calendar['color']),
644        'id'    => 'calendar-color',
645      ),
646    );
647
648    if ($this->cal->driver->alarms) {
649      $checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1));
650      $formfields['showalarms'] = array(
651        'label' => $this->cal->gettext('showalarms'),
652        'value' => $checkbox->show($this->calendar['showalarms'] ? 1 :0),
653        'id'    => 'calendar-showalarms',
654      );
655    }
656
657    // allow driver to extend or replace the form content
658    return html::tag('form', $attrib + array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'),
659      $this->cal->driver->calendar_form($this->action, $this->calendar, $formfields)
660    );
661  }
662
663  /**
664   *
665   */
666  function attendees_list($attrib = array())
667  {
668    // add "noreply" checkbox to attendees table only
669    $invitations = strpos($attrib['id'], 'attend') !== false;
670
671    $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
672    $table  = new html_table(array('cols' => 5 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
673
674    $table->add_header('role', $this->cal->gettext('role'));
675    $table->add_header('name', $this->cal->gettext($attrib['coltitle'] ?: 'attendee'));
676    $table->add_header('availability', $this->cal->gettext('availability'));
677    $table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
678    if ($invitations) {
679      $table->add_header(array('class' => 'invite', 'title' => $this->cal->gettext('sendinvitations')),
680        $invite->show(1) . html::label('edit-attendees-invite', html::span('inner', $this->cal->gettext('sendinvitations'))));
681    }
682    $table->add_header('options', '');
683
684    // hide invite column if disabled by config
685    $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->cal->defaults['calendar_itip_send_option']);
686    if ($invitations && !($itip_notify & 2)) {
687        $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']);
688        $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css));
689    }
690
691    return $table->show($attrib);
692  }
693
694  /**
695   *
696   */
697  function attendees_form($attrib = array())
698  {
699    $input    = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'class' => 'form-control'));
700    $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment', 'class' => 'form-control',
701        'rows' => 4, 'cols' => 55, 'title' => $this->cal->gettext('itipcommenttitle')));
702
703    return html::div($attrib,
704      html::div('form-searchbar', $input->show() . " " .
705        html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " .
706        html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) .
707      html::p('attendees-commentbox', html::label('edit-attendees-comment', $this->cal->gettext('itipcomment')) . $textarea->show())
708    );
709  }
710
711  /**
712   *
713   */
714  function resources_form($attrib = array())
715  {
716    $input = new html_inputfield(array('name' => 'resource', 'id' => 'edit-resource-name', 'class' => 'form-control'));
717
718    return html::div($attrib,
719      html::div('form-searchbar', $input->show() . " " .
720        html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-add', 'value' => $this->cal->gettext('addresource'))) . " " .
721        html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-find', 'value' => $this->cal->gettext('findresources').'...')))
722      );
723  }
724
725  /**
726   *
727   */
728  function resources_list($attrib = array())
729  {
730    $attrib += array('id' => 'calendar-resources-list');
731
732    $this->rc->output->add_gui_object('resourceslist', $attrib['id']);
733
734    return html::tag('ul', $attrib, '', html::$common_attrib);
735  }
736
737  /**
738   *
739   */
740  public function resource_info($attrib = array())
741  {
742    $attrib += array('id' => 'calendar-resources-info');
743
744    $this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
745    $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
746
747    // copy address book labels for owner details to client
748    $this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address');
749
750    $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border');
751
752    return html::tag('table', $attrib,
753        html::tag('tbody', null, ''), $table_attrib) .
754
755      html::tag('table', array('id' => $attrib['id'] . '-owner', 'style' => 'display:none') + $attrib,
756        html::tag('thead', null,
757          html::tag('tr', null,
758            html::tag('td', array('colspan' => 2), rcube::Q($this->cal->gettext('resourceowner')))
759          )
760        ) .
761        html::tag('tbody', null, ''),
762        $table_attrib);
763  }
764
765  /**
766   *
767   */
768  public function resource_calendar($attrib = array())
769  {
770    $attrib += array('id' => 'calendar-resources-calendar');
771
772    $this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']);
773
774    return html::div($attrib, '');
775  }
776
777  /**
778   * GUI object 'searchform' for the resource finder dialog
779   *
780   * @param array Named parameters
781   * @return string HTML code for the gui object
782   */
783  function resources_search_form($attrib)
784  {
785    $attrib += array(
786        'command'       => 'search-resource',
787        'reset-command' => 'reset-resource-search',
788        'id'            => 'rcmcalresqsearchbox',
789        'autocomplete'  => 'off',
790        'form-name'     => 'rcmcalresoursqsearchform',
791        'gui-object'    => 'resourcesearchform',
792    );
793
794    // add form tag around text field
795    return $this->rc->output->search_form($attrib);
796  }
797
798  /**
799   *
800   */
801  function attendees_freebusy_table($attrib = array())
802  {
803    $table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0));
804    $table->add('attendees',
805      html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) .
806      html::div('timesheader', '&nbsp;') .
807      html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
808    );
809    $table->add('times',
810      html::div('scroll',
811        html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) .
812        html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), '&nbsp;')
813      )
814    );
815
816    return $table->show($attrib);
817  }
818
819  /**
820   *
821   */
822  function event_invitebox($attrib = array())
823  {
824    if ($this->cal->event) {
825      return html::div($attrib,
826        $this->cal->itip->itip_object_details_table($this->cal->event, $this->cal->itip->gettext('itipinvitation')) .
827        $this->cal->invitestatus
828      );
829    }
830
831    return '';
832  }
833
834  function event_rsvp_buttons($attrib = array())
835  {
836    $actions = array('accepted','tentative','declined');
837    if ($attrib['delegate'] !== 'false')
838      $actions[] = 'delegated';
839
840    return $this->cal->itip->itip_rsvp_buttons($attrib, $actions);
841  }
842
843}
844