1<?php 2 3/** 4 * Kolab address book 5 * 6 * Sample plugin to add a new address book source with data from Kolab storage 7 * It provides also a possibilities to manage contact folders 8 * (create/rename/delete/acl) directly in Addressbook UI. 9 * 10 * @version @package_version@ 11 * @author Thomas Bruederli <bruederli@kolabsys.com> 12 * @author Aleksander Machniak <machniak@kolabsys.com> 13 * 14 * Copyright (C) 2011-2015, Kolab Systems AG <contact@kolabsys.com> 15 * 16 * This program is free software: you can redistribute it and/or modify 17 * it under the terms of the GNU Affero General Public License as 18 * published by the Free Software Foundation, either version 3 of the 19 * License, or (at your option) any later version. 20 * 21 * This program is distributed in the hope that it will be useful, 22 * but WITHOUT ANY WARRANTY; without even the implied warranty of 23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 * GNU Affero General Public License for more details. 25 * 26 * You should have received a copy of the GNU Affero General Public License 27 * along with this program. If not, see <http://www.gnu.org/licenses/>. 28 */ 29 30class kolab_addressbook extends rcube_plugin 31{ 32 public $task = '?(?!logout).*'; 33 34 private $sources; 35 private $folders; 36 private $rc; 37 private $ui; 38 39 public $bonnie_api = false; 40 41 const GLOBAL_FIRST = 0; 42 const PERSONAL_FIRST = 1; 43 const GLOBAL_ONLY = 2; 44 const PERSONAL_ONLY = 3; 45 46 /** 47 * Startup method of a Roundcube plugin 48 */ 49 public function init() 50 { 51 require_once(dirname(__FILE__) . '/lib/rcube_kolab_contacts.php'); 52 53 $this->rc = rcube::get_instance(); 54 55 // load required plugin 56 $this->require_plugin('libkolab'); 57 58 // register hooks 59 $this->add_hook('addressbooks_list', array($this, 'address_sources')); 60 $this->add_hook('addressbook_get', array($this, 'get_address_book')); 61 $this->add_hook('config_get', array($this, 'config_get')); 62 63 if ($this->rc->task == 'addressbook') { 64 $this->add_texts('localization'); 65 $this->add_hook('contact_form', array($this, 'contact_form')); 66 $this->add_hook('contact_photo', array($this, 'contact_photo')); 67 $this->add_hook('template_object_directorylist', array($this, 'directorylist_html')); 68 69 // Plugin actions 70 $this->register_action('plugin.book', array($this, 'book_actions')); 71 $this->register_action('plugin.book-save', array($this, 'book_save')); 72 $this->register_action('plugin.book-search', array($this, 'book_search')); 73 $this->register_action('plugin.book-subscribe', array($this, 'book_subscribe')); 74 75 $this->register_action('plugin.contact-changelog', array($this, 'contact_changelog')); 76 $this->register_action('plugin.contact-diff', array($this, 'contact_diff')); 77 $this->register_action('plugin.contact-restore', array($this, 'contact_restore')); 78 79 // get configuration for the Bonnie API 80 $this->bonnie_api = libkolab::get_bonnie_api(); 81 82 // Load UI elements 83 if ($this->api->output->type == 'html') { 84 $this->load_config(); 85 require_once($this->home . '/lib/kolab_addressbook_ui.php'); 86 $this->ui = new kolab_addressbook_ui($this); 87 88 if ($this->bonnie_api) { 89 $this->add_button(array( 90 'command' => 'contact-history-dialog', 91 'class' => 'history contact-history disabled', 92 'classact' => 'history contact-history active', 93 'innerclass' => 'icon inner', 94 'label' => 'kolab_addressbook.showhistory', 95 'type' => 'link-menuitem' 96 ), 'contactmenu'); 97 } 98 } 99 } 100 else if ($this->rc->task == 'settings') { 101 $this->add_texts('localization'); 102 $this->add_hook('preferences_list', array($this, 'prefs_list')); 103 $this->add_hook('preferences_save', array($this, 'prefs_save')); 104 } 105 106 $this->add_hook('folder_delete', array($this, 'prefs_folder_delete')); 107 $this->add_hook('folder_rename', array($this, 'prefs_folder_rename')); 108 $this->add_hook('folder_update', array($this, 'prefs_folder_update')); 109 } 110 111 /** 112 * Handler for the addressbooks_list hook. 113 * 114 * This will add all instances of available Kolab-based address books 115 * to the list of address sources of Roundcube. 116 * This will also hide some addressbooks according to kolab_addressbook_prio setting. 117 * 118 * @param array $p Hash array with hook parameters 119 * 120 * @return array Hash array with modified hook parameters 121 */ 122 public function address_sources($p) 123 { 124 $abook_prio = $this->addressbook_prio(); 125 126 // Disable all global address books 127 // Assumes that all non-kolab_addressbook sources are global 128 if ($abook_prio == self::PERSONAL_ONLY) { 129 $p['sources'] = array(); 130 } 131 132 $sources = array(); 133 foreach ($this->_list_sources() as $abook_id => $abook) { 134 // register this address source 135 $sources[$abook_id] = $this->abook_prop($abook_id, $abook); 136 137 // flag folders with 'i' right as writeable 138 if ($this->rc->action == 'add' && strpos($abook->rights, 'i') !== false) { 139 $sources[$abook_id]['readonly'] = false; 140 } 141 } 142 143 // Add personal address sources to the list 144 if ($abook_prio == self::PERSONAL_FIRST) { 145 // $p['sources'] = array_merge($sources, $p['sources']); 146 // Don't use array_merge(), because if you have folders name 147 // that resolve to numeric identifier it will break output array keys 148 foreach ($p['sources'] as $idx => $value) 149 $sources[$idx] = $value; 150 $p['sources'] = $sources; 151 } 152 else { 153 // $p['sources'] = array_merge($p['sources'], $sources); 154 foreach ($sources as $idx => $value) 155 $p['sources'][$idx] = $value; 156 } 157 158 return $p; 159 } 160 161 /** 162 * Helper method to build a hash array of address book properties 163 */ 164 protected function abook_prop($id, $abook) 165 { 166 if ($abook->virtual) { 167 return array( 168 'id' => $id, 169 'name' => $abook->get_name(), 170 'listname' => $abook->get_foldername(), 171 'group' => $abook instanceof kolab_storage_folder_user ? 'user' : $abook->get_namespace(), 172 'readonly' => true, 173 'rights' => 'l', 174 'kolab' => true, 175 'virtual' => true, 176 ); 177 } 178 else { 179 return array( 180 'id' => $id, 181 'name' => $abook->get_name(), 182 'listname' => $abook->get_foldername(), 183 'readonly' => $abook->readonly, 184 'rights' => $abook->rights, 185 'groups' => $abook->groups, 186 'undelete' => $abook->undelete && $this->rc->config->get('undo_timeout'), 187 'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name 188 'group' => $abook->get_namespace(), 189 'subscribed' => $abook->is_subscribed(), 190 'carddavurl' => $abook->get_carddav_url(), 191 'removable' => true, 192 'kolab' => true, 193 'audittrail' => !empty($this->bonnie_api), 194 ); 195 } 196 } 197 198 /** 199 * 200 */ 201 public function directorylist_html($args) 202 { 203 $out = ''; 204 $jsdata = array(); 205 $sources = (array)$this->rc->get_address_sources(); 206 207 // list all non-kolab sources first (also exclude hidden sources) 208 $filter = function($source){ return empty($source['kolab']) && empty($source['hidden']); }; 209 foreach (array_filter($sources, $filter) as $j => $source) { 210 $id = strval(strlen($source['id']) ? $source['id'] : $j); 211 $out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>'; 212 } 213 214 // render a hierarchical list of kolab contact folders 215 kolab_storage::folder_hierarchy($this->folders, $tree); 216 if ($tree && !empty($tree->children)) { 217 $out .= $this->folder_tree_html($tree, $sources, $jsdata); 218 } 219 220 $this->rc->output->set_env('contactgroups', array_filter($jsdata, function($src){ return $src['type'] == 'group'; })); 221 $this->rc->output->set_env('address_sources', array_filter($jsdata, function($src){ return $src['type'] != 'group'; })); 222 223 $args['content'] = html::tag('ul', $args, $out, html::$common_attrib); 224 return $args; 225 } 226 227 /** 228 * Return html for a structured list <ul> for the folder tree 229 */ 230 public function folder_tree_html($node, $data, &$jsdata) 231 { 232 $out = ''; 233 foreach ($node->children as $folder) { 234 $id = $folder->id; 235 $source = $data[$id]; 236 $is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false; 237 238 if ($folder->virtual) { 239 $source = $this->abook_prop($folder->id, $folder); 240 } 241 else if (empty($source)) { 242 $this->sources[$id] = new rcube_kolab_contacts($folder->name); 243 $source = $this->abook_prop($id, $this->sources[$id]); 244 } 245 246 $content = $this->addressbook_list_item($id, $source, $jsdata); 247 248 if (!empty($folder->children)) { 249 $child_html = $this->folder_tree_html($folder, $data, $jsdata); 250 251 // copy group items... 252 if (preg_match('!<ul[^>]*>(.*)</ul>\n*$!Ums', $content, $m)) { 253 $child_html = $m[1] . $child_html; 254 $content = substr($content, 0, -strlen($m[0]) - 1); 255 } 256 // ... and re-create the subtree 257 if (!empty($child_html)) { 258 $content .= html::tag('ul', array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), $child_html); 259 } 260 } 261 262 $out .= $content . '</li>'; 263 } 264 265 return $out; 266 } 267 268 /** 269 * 270 */ 271 protected function addressbook_list_item($id, $source, &$jsdata, $search_mode = false) 272 { 273 $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); 274 275 if (!$source['virtual']) { 276 $jsdata[$id] = $source; 277 $jsdata[$id]['name'] = html_entity_decode($source['name'], ENT_NOQUOTES, RCUBE_CHARSET); 278 } 279 280 // set class name(s) 281 $classes = array('addressbook'); 282 if ($source['group']) 283 $classes[] = $source['group']; 284 if ($current === $id) 285 $classes[] = 'selected'; 286 if ($source['readonly']) 287 $classes[] = 'readonly'; 288 if ($source['virtual']) 289 $classes[] = 'virtual'; 290 if ($source['class_name']) 291 $classes[] = $source['class_name']; 292 293 $name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id); 294 $label_id = 'kabt:' . $id; 295 $inner = ($source['virtual'] ? 296 html::a(array('tabindex' => '0'), $name) : 297 html::a(array( 298 'href' => $this->rc->url(array('_source' => $id)), 299 'rel' => $source['id'], 300 'id' => $label_id, 301 'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)", 302 ), $name) 303 ); 304 305 if (isset($source['subscribed'])) { 306 $inner .= html::span(array( 307 'class' => 'subscribed', 308 'title' => $this->gettext('foldersubscribe'), 309 'role' => 'checkbox', 310 'aria-checked' => $source['subscribed'] ? 'true' : 'false', 311 ), ''); 312 } 313 314 // don't wrap in <li> but add a checkbox for search results listing 315 if ($search_mode) { 316 $jsdata[$id]['group'] = join(' ', $classes); 317 318 if (!$source['virtual']) { 319 $inner .= html::tag('input', array( 320 'type' => 'checkbox', 321 'name' => '_source[]', 322 'value' => $id, 323 'checked' => false, 324 'aria-labelledby' => $label_id, 325 )); 326 } 327 return html::div(null, $inner); 328 } 329 330 $out .= html::tag('li', array( 331 'id' => 'rcmli' . rcube_utils::html_identifier($id, true), 332 'class' => join(' ', $classes), 333 'noclose' => true, 334 ), 335 html::div($source['subscribed'] ? 'subscribed' : null, $inner) 336 ); 337 338 $groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id); 339 if ($source['groups'] && function_exists('rcmail_contact_groups')) { 340 $groupdata = rcmail_contact_groups($groupdata); 341 } 342 343 $jsdata = $groupdata['jsdata']; 344 $out .= $groupdata['out']; 345 346 return $out; 347 } 348 349 /** 350 * Sets autocomplete_addressbooks option according to 351 * kolab_addressbook_prio setting extending list of address sources 352 * to be used for autocompletion. 353 */ 354 public function config_get($args) 355 { 356 if ($args['name'] != 'autocomplete_addressbooks' || $this->recurrent) { 357 return $args; 358 } 359 360 $abook_prio = $this->addressbook_prio(); 361 362 // Get the original setting, use temp flag to prevent from an infinite recursion 363 $this->recurrent = true; 364 $sources = $this->rc->config->get('autocomplete_addressbooks'); 365 $this->recurrent = false; 366 367 // Disable all global address books 368 // Assumes that all non-kolab_addressbook sources are global 369 if ($abook_prio == self::PERSONAL_ONLY) { 370 $sources = array(); 371 } 372 373 if (!is_array($sources)) { 374 $sources = array(); 375 } 376 377 $kolab_sources = array(); 378 foreach (array_keys($this->_list_sources()) as $abook_id) { 379 if (!in_array($abook_id, $sources)) 380 $kolab_sources[] = $abook_id; 381 } 382 383 // Add personal address sources to the list 384 if (!empty($kolab_sources)) { 385 if ($abook_prio == self::PERSONAL_FIRST) { 386 $sources = array_merge($kolab_sources, $sources); 387 } 388 else { 389 $sources = array_merge($sources, $kolab_sources); 390 } 391 } 392 393 $args['result'] = $sources; 394 395 return $args; 396 } 397 398 399 /** 400 * Getter for the rcube_addressbook instance 401 * 402 * @param array $p Hash array with hook parameters 403 * 404 * @return array Hash array with modified hook parameters 405 */ 406 public function get_address_book($p) 407 { 408 if ($p['id']) { 409 $id = kolab_storage::id_decode($p['id']); 410 $folder = kolab_storage::get_folder($id); 411 412 // try with unencoded (old-style) identifier 413 if ((!$folder || $folder->type != 'contact') && $id != $p['id']) { 414 $folder = kolab_storage::get_folder($p['id']); 415 } 416 417 if ($folder && $folder->type == 'contact') { 418 $p['instance'] = new rcube_kolab_contacts($folder->name); 419 420 // flag source as writeable if 'i' right is given 421 if ($p['writeable'] && $this->rc->action == 'save' && strpos($p['instance']->rights, 'i') !== false) { 422 $p['instance']->readonly = false; 423 } 424 else if ($this->rc->action == 'delete' && strpos($p['instance']->rights, 't') !== false) { 425 $p['instance']->readonly = false; 426 } 427 } 428 } 429 430 return $p; 431 } 432 433 434 private function _list_sources() 435 { 436 // already read sources 437 if (isset($this->sources)) 438 return $this->sources; 439 440 kolab_storage::$encode_ids = true; 441 $this->sources = array(); 442 $this->folders = array(); 443 444 $abook_prio = $this->addressbook_prio(); 445 446 // Personal address source(s) disabled? 447 if ($abook_prio == self::GLOBAL_ONLY) { 448 return $this->sources; 449 } 450 451 // get all folders that have "contact" type 452 $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact')); 453 454 if (PEAR::isError($folders)) { 455 rcube::raise_error(array( 456 'code' => 600, 'type' => 'php', 457 'file' => __FILE__, 'line' => __LINE__, 458 'message' => "Failed to list contact folders from Kolab server:" . $folders->getMessage()), 459 true, false); 460 } 461 else { 462 // we need at least one folder to prevent from errors in Roundcube core 463 // when there's also no sql nor ldap addressbook (Bug #2086) 464 if (empty($folders)) { 465 if ($folder = kolab_storage::create_default_folder('contact')) { 466 $folders = array(new kolab_storage_folder($folder, 'contact')); 467 } 468 } 469 470 // convert to UTF8 and sort 471 foreach ($folders as $folder) { 472 // create instance of rcube_contacts 473 $abook_id = $folder->id; 474 $abook = new rcube_kolab_contacts($folder->name); 475 $this->sources[$abook_id] = $abook; 476 $this->folders[$abook_id] = $folder; 477 } 478 } 479 480 return $this->sources; 481 } 482 483 484 /** 485 * Plugin hook called before rendering the contact form or detail view 486 * 487 * @param array $p Hash array with hook parameters 488 * 489 * @return array Hash array with modified hook parameters 490 */ 491 public function contact_form($p) 492 { 493 // none of our business 494 if (!is_object($GLOBALS['CONTACTS']) || !is_a($GLOBALS['CONTACTS'], 'rcube_kolab_contacts')) 495 return $p; 496 497 // extend the list of contact fields to be displayed in the 'personal' section 498 if (is_array($p['form']['personal'])) { 499 $p['form']['personal']['content']['profession'] = array('size' => 40); 500 $p['form']['personal']['content']['children'] = array('size' => 40); 501 $p['form']['personal']['content']['freebusyurl'] = array('size' => 40); 502 $p['form']['personal']['content']['pgppublickey'] = array('size' => 70); 503 $p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70); 504 505 // re-order fields according to the coltypes list 506 $p['form']['contact']['content'] = $this->_sort_form_fields($p['form']['contact']['content'], $GLOBALS['CONTACTS']); 507 $p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content'], $GLOBALS['CONTACTS']); 508 509 /* define a separate section 'settings' 510 $p['form']['settings'] = array( 511 'name' => $this->gettext('settings'), 512 'content' => array( 513 'freebusyurl' => array('size' => 40, 'visible' => true), 514 'pgppublickey' => array('size' => 70, 'visible' => true), 515 'pkcs7publickey' => array('size' => 70, 'visible' => false), 516 ) 517 ); 518 */ 519 } 520 521 if ($this->bonnie_api && $this->rc->action == 'show' && empty($p['record']['rev'])) { 522 $this->rc->output->set_env('kolab_audit_trail', true); 523 } 524 525 return $p; 526 } 527 528 /** 529 * Plugin hook for the contact photo image 530 */ 531 public function contact_photo($p) 532 { 533 // add photo data from old revision inline as data url 534 if (!empty($p['record']['rev']) && !empty($p['data'])) { 535 $p['url'] = 'data:image/gif;base64,' . base64_encode($p['data']); 536 } 537 538 return $p; 539 } 540 541 /** 542 * Handler for contact audit trail changelog requests 543 */ 544 public function contact_changelog() 545 { 546 if (empty($this->bonnie_api)) { 547 return false; 548 } 549 550 $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true); 551 $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); 552 553 list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source); 554 555 $result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null; 556 if (is_array($result) && $result['uid'] == $uid) { 557 if (is_array($result['changes'])) { 558 $rcmail = $this->rc; 559 $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format'); 560 array_walk($result['changes'], function(&$change) use ($rcmail, $dtformat) { 561 if ($change['date']) { 562 $dt = rcube_utils::anytodatetime($change['date']); 563 if ($dt instanceof DateTime) { 564 $change['date'] = $rcmail->format_date($dt, $dtformat); 565 } 566 } 567 }); 568 } 569 $this->rc->output->command('contact_render_changelog', $result['changes']); 570 } 571 else { 572 $this->rc->output->command('contact_render_changelog', false); 573 } 574 575 $this->rc->output->send(); 576 } 577 578 /** 579 * Handler for audit trail diff view requests 580 */ 581 public function contact_diff() 582 { 583 if (empty($this->bonnie_api)) { 584 return false; 585 } 586 587 $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true); 588 $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); 589 $rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST); 590 $rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST); 591 592 list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source); 593 594 $result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid); 595 if (is_array($result) && $result['uid'] == $uid) { 596 $result['rev1'] = $rev1; 597 $result['rev2'] = $rev2; 598 $result['cid'] = $contact; 599 600 // convert some properties, similar to rcube_kolab_contacts::_to_rcube_contact() 601 $keymap = array( 602 'lastmodified-date' => 'changed', 603 'additional' => 'middlename', 604 'fn' => 'name', 605 'tel' => 'phone', 606 'url' => 'website', 607 'bday' => 'birthday', 608 'note' => 'notes', 609 'role' => 'profession', 610 'title' => 'jobtitle', 611 ); 612 613 $propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number'); 614 $date_format = $this->rc->config->get('date_format', 'Y-m-d'); 615 616 // map kolab object properties to keys and values the client expects 617 array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) { 618 if (array_key_exists($change['property'], $keymap)) { 619 $change['property'] = $keymap[$change['property']]; 620 } 621 622 // format date-time values 623 if ($change['property'] == 'created' || $change['property'] == 'changed') { 624 if ($old_ = rcube_utils::anytodatetime($change['old'])) { 625 $change['old_'] = $this->rc->format_date($old_); 626 } 627 if ($new_ = rcube_utils::anytodatetime($change['new'])) { 628 $change['new_'] = $this->rc->format_date($new_); 629 } 630 } 631 // format dates 632 else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') { 633 if ($old_ = rcube_utils::anytodatetime($change['old'])) { 634 $change['old_'] = $this->rc->format_date($old_, $date_format); 635 } 636 if ($new_ = rcube_utils::anytodatetime($change['new'])) { 637 $change['new_'] = $this->rc->format_date($new_, $date_format); 638 } 639 } 640 // convert email, website, phone values 641 else if (array_key_exists($change['property'], $propmap)) { 642 $propname = $propmap[$change['property']]; 643 foreach (array('old','new') as $k) { 644 $k_ = $k . '_'; 645 if (!empty($change[$k])) { 646 $change[$k_] = html::quote($change[$k][$propname] ?: '--'); 647 if ($change[$k]['type']) { 648 $change[$k_] .= ' ' . html::span('subtype', rcmail_get_type_label($change[$k]['type'])); 649 } 650 $change['ishtml'] = true; 651 } 652 } 653 } 654 // serialize address structs 655 if ($change['property'] == 'address') { 656 foreach (array('old','new') as $k) { 657 $k_ = $k . '_'; 658 $change[$k]['zipcode'] = $change[$k]['code']; 659 $template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}'); 660 $composite = array(); 661 foreach ($change[$k] as $p => $val) { 662 if (strlen($val)) 663 $composite['{'.$p.'}'] = $val; 664 } 665 $change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite)); 666 if ($change[$k]['type']) { 667 $change[$k_] .= html::div('subtype', rcmail_get_type_label($change[$k]['type'])); 668 } 669 $change['ishtml'] = true; 670 } 671 672 $change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true); 673 } 674 // localize gender values 675 else if ($change['property'] == 'gender') { 676 if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']); 677 if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']); 678 } 679 // translate 'key' entries in individual properties 680 else if ($change['property'] == 'key') { 681 $p = $change['old'] ?: $change['new']; 682 $t = $p['type']; 683 $change['property'] = $t . 'publickey'; 684 $change['old'] = $change['old'] ? $change['old']['key'] : ''; 685 $change['new'] = $change['new'] ? $change['new']['key'] : ''; 686 } 687 // compute a nice diff of notes 688 else if ($change['property'] == 'notes') { 689 $change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false); 690 } 691 }); 692 693 $this->rc->output->command('contact_show_diff', $result); 694 } 695 else { 696 $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error'); 697 } 698 699 $this->rc->output->send(); 700 } 701 702 /** 703 * Handler for audit trail revision restore requests 704 */ 705 public function contact_restore() 706 { 707 if (empty($this->bonnie_api)) { 708 return false; 709 } 710 711 $success = false; 712 $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true); 713 $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST); 714 $rev = rcube_utils::get_input_value('rev', rcube_utils::INPUT_POST); 715 716 list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source, $folder); 717 718 if ($folder && ($raw_msg = $this->bonnie_api->rawdata('contact', $uid, $rev, $mailbox))) { 719 $imap = $this->rc->get_storage(); 720 721 // insert $raw_msg as new message 722 if ($imap->save_message($folder->name, $raw_msg, null, false)) { 723 $success = true; 724 725 // delete old revision from imap and cache 726 $imap->delete_message($msguid, $folder->name); 727 $folder->cache->set($msguid, false); 728 $this->cache = array(); 729 } 730 } 731 732 if ($success) { 733 $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rev))), 'confirmation'); 734 $this->rc->output->command('close_contact_history_dialog', $contact); 735 } 736 else { 737 $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error'); 738 } 739 740 $this->rc->output->send(); 741 } 742 743 /** 744 * Get a previous revision of the given contact record from the Bonnie API 745 */ 746 public function get_revision($cid, $source, $rev) 747 { 748 if (empty($this->bonnie_api)) { 749 return false; 750 } 751 752 list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($cid, $source); 753 754 // call Bonnie API 755 $result = $this->bonnie_api->get('contact', $uid, $rev, $mailbox, $msguid); 756 if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) { 757 $format = kolab_format::factory('contact'); 758 $format->load($result['xml']); 759 $rec = $format->to_array(); 760 761 if ($format->is_valid()) { 762 $rec['rev'] = $result['rev']; 763 return $rec; 764 } 765 } 766 767 return false; 768 } 769 770 771 /** 772 * Helper method to resolved the given contact identifier into uid and mailbox 773 * 774 * @return array (uid,mailbox,msguid) tuple 775 */ 776 private function _resolve_contact_identity($id, $abook, &$folder = null) 777 { 778 $mailbox = $msguid = null; 779 780 $source = $this->get_address_book(array('id' => $abook)); 781 if ($source['instance']) { 782 $uid = $source['instance']->id2uid($id); 783 $list = kolab_storage::id_decode($abook); 784 } 785 else { 786 return array(null, $mailbox, $msguid); 787 } 788 789 // get resolve message UID and mailbox identifier 790 if ($folder = kolab_storage::get_folder($list)) { 791 $mailbox = $folder->get_mailbox_id(); 792 $msguid = $folder->cache->uid2msguid($uid); 793 } 794 795 return array($uid, $mailbox, $msguid); 796 } 797 798 /** 799 * 800 */ 801 private function _sort_form_fields($contents, $source) 802 { 803 $block = array(); 804 805 foreach (array_keys($source->coltypes) as $col) { 806 if (isset($contents[$col])) 807 $block[$col] = $contents[$col]; 808 } 809 810 return $block; 811 } 812 813 814 /** 815 * Handler for user preferences form (preferences_list hook) 816 * 817 * @param array $args Hash array with hook parameters 818 * 819 * @return array Hash array with modified hook parameters 820 */ 821 public function prefs_list($args) 822 { 823 if ($args['section'] != 'addressbook') { 824 return $args; 825 } 826 827 $ldap_public = $this->rc->config->get('ldap_public'); 828 829 // Hide option if there's no global addressbook 830 if (empty($ldap_public)) { 831 return $args; 832 } 833 834 // Check that configuration is not disabled 835 $dont_override = (array) $this->rc->config->get('dont_override', array()); 836 $prio = $this->addressbook_prio(); 837 838 if (!in_array('kolab_addressbook_prio', $dont_override)) { 839 // Load localization 840 $this->add_texts('localization'); 841 842 $field_id = '_kolab_addressbook_prio'; 843 $select = new html_select(array('name' => $field_id, 'id' => $field_id)); 844 845 $select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST); 846 $select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST); 847 $select->add($this->gettext('globalonly'), self::GLOBAL_ONLY); 848 $select->add($this->gettext('personalonly'), self::PERSONAL_ONLY); 849 850 $args['blocks']['main']['options']['kolab_addressbook_prio'] = array( 851 'title' => html::label($field_id, rcube::Q($this->gettext('addressbookprio'))), 852 'content' => $select->show($prio), 853 ); 854 } 855 856 return $args; 857 } 858 859 /** 860 * Handler for user preferences save (preferences_save hook) 861 * 862 * @param array $args Hash array with hook parameters 863 * 864 * @return array Hash array with modified hook parameters 865 */ 866 public function prefs_save($args) 867 { 868 if ($args['section'] != 'addressbook') { 869 return $args; 870 } 871 872 // Check that configuration is not disabled 873 $dont_override = (array) $this->rc->config->get('dont_override', array()); 874 $key = 'kolab_addressbook_prio'; 875 876 if (!in_array('kolab_addressbook_prio', $dont_override) || !isset($_POST['_'.$key])) { 877 $args['prefs'][$key] = (int) rcube_utils::get_input_value('_'.$key, rcube_utils::INPUT_POST); 878 } 879 880 return $args; 881 } 882 883 884 /** 885 * Handler for plugin actions 886 */ 887 public function book_actions() 888 { 889 $action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC)); 890 891 if ($action == 'create') { 892 $this->ui->book_edit(); 893 } 894 else if ($action == 'edit') { 895 $this->ui->book_edit(); 896 } 897 else if ($action == 'delete') { 898 $this->book_delete(); 899 } 900 } 901 902 903 /** 904 * Handler for address book create/edit form submit 905 */ 906 public function book_save() 907 { 908 $prop = array( 909 'name' => trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST)), 910 'oldname' => trim(rcube_utils::get_input_value('_oldname', rcube_utils::INPUT_POST, true)), // UTF7-IMAP 911 'parent' => trim(rcube_utils::get_input_value('_parent', rcube_utils::INPUT_POST, true)), // UTF7-IMAP 912 'type' => 'contact', 913 'subscribed' => true, 914 ); 915 916 $result = $error = false; 917 $type = strlen($prop['oldname']) ? 'update' : 'create'; 918 $prop = $this->rc->plugins->exec_hook('addressbook_'.$type, $prop); 919 920 if (!$prop['abort']) { 921 if ($newfolder = kolab_storage::folder_update($prop)) { 922 $folder = $newfolder; 923 $result = true; 924 } 925 else { 926 $error = kolab_storage::$last_error; 927 } 928 } 929 else { 930 $result = $prop['result']; 931 $folder = $prop['name']; 932 } 933 934 if ($result) { 935 $kolab_folder = kolab_storage::get_folder($folder); 936 937 // get folder/addressbook properties 938 $abook = new rcube_kolab_contacts($folder); 939 $props = $this->abook_prop(kolab_storage::folder_id($folder, true), $abook); 940 $props['parent'] = kolab_storage::folder_id($kolab_folder->get_parent(), true); 941 942 $this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation'); 943 $this->rc->output->command('book_update', $props, kolab_storage::folder_id($prop['oldname'], true)); 944 } 945 else { 946 if (!$error) { 947 $error = $plugin['message'] ? $plugin['message'] : 'kolab_addressbook.book'.$type.'error'; 948 } 949 950 $this->rc->output->show_message($error, 'error'); 951 } 952 953 $this->rc->output->send('iframe'); 954 } 955 956 /** 957 * 958 */ 959 public function book_search() 960 { 961 $results = array(); 962 $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC); 963 $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC); 964 965 kolab_storage::$encode_ids = true; 966 $search_more_results = false; 967 $this->sources = array(); 968 $this->folders = array(); 969 970 // find unsubscribed IMAP folders that have "event" type 971 if ($source == 'folders') { 972 foreach ((array)kolab_storage::search_folders('contact', $query, array('other')) as $folder) { 973 $this->folders[$folder->id] = $folder; 974 $this->sources[$folder->id] = new rcube_kolab_contacts($folder->name); 975 } 976 } 977 // search other user's namespace via LDAP 978 else if ($source == 'users') { 979 $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number 980 foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) { 981 $folders = array(); 982 // search for contact folders shared by this user 983 foreach (kolab_storage::list_user_folders($user, 'contact', false) as $foldername) { 984 $folders[] = new kolab_storage_folder($foldername, 'contact'); 985 } 986 987 if (count($folders)) { 988 $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user); 989 $this->folders[$userfolder->id] = $userfolder; 990 $this->sources[$userfolder->id] = $userfolder; 991 992 foreach ($folders as $folder) { 993 $this->folders[$folder->id] = $folder; 994 $this->sources[$folder->id] = new rcube_kolab_contacts($folder->name);; 995 $count++; 996 } 997 } 998 999 if ($count >= $limit) { 1000 $search_more_results = true; 1001 break; 1002 } 1003 } 1004 } 1005 1006 $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); 1007 1008 // build results list 1009 foreach ($this->sources as $id => $source) { 1010 $folder = $this->folders[$id]; 1011 $imap_path = explode($delim, $folder->name); 1012 1013 // find parent 1014 do { 1015 array_pop($imap_path); 1016 $parent_id = kolab_storage::folder_id(join($delim, $imap_path)); 1017 } 1018 while (count($imap_path) > 1 && !$this->folders[$parent_id]); 1019 1020 // restore "real" parent ID 1021 if ($parent_id && !$this->folders[$parent_id]) { 1022 $parent_id = kolab_storage::folder_id($folder->get_parent()); 1023 } 1024 1025 $prop = $this->abook_prop($id, $source); 1026 $prop['parent'] = $parent_id; 1027 1028 $html = $this->addressbook_list_item($id, $prop, $jsdata, true); 1029 unset($prop['group']); 1030 $prop += (array)$jsdata[$id]; 1031 $prop['html'] = $html; 1032 1033 $results[] = $prop; 1034 } 1035 1036 // report more results available 1037 if ($search_more_results) { 1038 $this->rc->output->show_message('autocompletemore', 'notice'); 1039 } 1040 1041 $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC)); 1042 } 1043 1044 /** 1045 * 1046 */ 1047 public function book_subscribe() 1048 { 1049 $success = false; 1050 $id = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); 1051 1052 if ($id && ($folder = kolab_storage::get_folder(kolab_storage::id_decode($id)))) { 1053 if (isset($_POST['_permanent'])) 1054 $success |= $folder->subscribe(intval($_POST['_permanent'])); 1055 if (isset($_POST['_active'])) 1056 $success |= $folder->activate(intval($_POST['_active'])); 1057 1058 // list groups for this address book 1059 if (!empty($_POST['_groups'])) { 1060 $abook = new rcube_kolab_contacts($folder->name); 1061 foreach ((array)$abook->list_groups() as $prop) { 1062 $prop['source'] = $id; 1063 $prop['id'] = $prop['ID']; 1064 unset($prop['ID']); 1065 $this->rc->output->command('insert_contact_group', $prop); 1066 } 1067 } 1068 } 1069 1070 if ($success) { 1071 $this->rc->output->show_message('successfullysaved', 'confirmation'); 1072 } 1073 else { 1074 $this->rc->output->show_message($this->gettext('errorsaving'), 'error'); 1075 } 1076 1077 $this->rc->output->send(); 1078 } 1079 1080 1081 /** 1082 * Handler for address book delete action (AJAX) 1083 */ 1084 private function book_delete() 1085 { 1086 $folder = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true, 'UTF7-IMAP')); 1087 1088 if (kolab_storage::folder_delete($folder)) { 1089 $storage = $this->rc->get_storage(); 1090 $delimiter = $storage->get_hierarchy_delimiter(); 1091 1092 $this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation'); 1093 $this->rc->output->set_env('pagecount', 0); 1094 $this->rc->output->command('set_rowcount', rcmail_get_rowcount_text(new rcube_result_set())); 1095 $this->rc->output->command('set_env', 'delimiter', $delimiter); 1096 $this->rc->output->command('list_contacts_clear'); 1097 $this->rc->output->command('book_delete_done', kolab_storage::folder_id($folder, true)); 1098 } 1099 else { 1100 $this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error'); 1101 } 1102 1103 $this->rc->output->send(); 1104 } 1105 1106 /** 1107 * Returns value of kolab_addressbook_prio setting 1108 */ 1109 private function addressbook_prio() 1110 { 1111 // Load configuration 1112 if (!$this->config_loaded) { 1113 $this->load_config(); 1114 $this->config_loaded = true; 1115 } 1116 1117 $abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio'); 1118 1119 // Make sure any global addressbooks are defined 1120 if ($abook_prio == 0 || $abook_prio == 2) { 1121 $ldap_public = $this->rc->config->get('ldap_public'); 1122 1123 if (empty($ldap_public)) { 1124 $abook_prio = 1; 1125 } 1126 } 1127 1128 return $abook_prio; 1129 } 1130 1131 /** 1132 * Hook for (contact) folder deletion 1133 */ 1134 function prefs_folder_delete($args) 1135 { 1136 // ignore... 1137 if ($args['abort'] && !$args['result']) { 1138 return $args; 1139 } 1140 1141 $this->_contact_folder_rename($args['name'], false); 1142 } 1143 1144 /** 1145 * Hook for (contact) folder renaming 1146 */ 1147 function prefs_folder_rename($args) 1148 { 1149 // ignore... 1150 if ($args['abort'] && !$args['result']) { 1151 return $args; 1152 } 1153 1154 $this->_contact_folder_rename($args['oldname'], $args['newname']); 1155 } 1156 1157 /** 1158 * Hook for (contact) folder updates. Forward to folder_rename handler if name was changed 1159 */ 1160 function prefs_folder_update($args) 1161 { 1162 // ignore... 1163 if ($args['abort'] && !$args['result']) { 1164 return $args; 1165 } 1166 1167 if ($args['record']['name'] != $args['record']['oldname']) { 1168 $this->_contact_folder_rename($args['record']['oldname'], $args['record']['name']); 1169 } 1170 } 1171 1172 /** 1173 * Apply folder renaming or deletion to the registered birthday calendar address books 1174 */ 1175 private function _contact_folder_rename($oldname, $newname = false) 1176 { 1177 $update = false; 1178 $delimiter = $this->rc->get_storage()->get_hierarchy_delimiter(); 1179 $bday_addressbooks = (array)$this->rc->config->get('calendar_birthday_adressbooks', array()); 1180 1181 foreach ($bday_addressbooks as $i => $id) { 1182 $folder_name = kolab_storage::id_decode($id); 1183 if ($oldname === $folder_name || strpos($folder_name, $oldname.$delimiter) === 0) { 1184 if ($newname) { // rename 1185 $new_folder = $newname . substr($folder_name, strlen($oldname)); 1186 $bday_addressbooks[$i] = kolab_storage::id_encode($new_folder); 1187 } 1188 else { // delete 1189 unset($bday_addressbooks[$i]); 1190 } 1191 $update = true; 1192 } 1193 } 1194 1195 if ($update) { 1196 $this->rc->user->save_prefs(array('calendar_birthday_adressbooks' => $bday_addressbooks)); 1197 } 1198 } 1199 1200} 1201