1<?php 2 3/** 4 +-----------------------------------------------------------------------+ 5 | This file is part of the Roundcube Webmail client | 6 | | 7 | Copyright (C) The Roundcube Dev Team | 8 | | 9 | Licensed under the GNU General Public License version 3 or | 10 | any later version with exceptions for skins & plugins. | 11 | See the README file for a full license statement. | 12 | | 13 | PURPOSE: | 14 | Provide addressbook functionality and GUI objects | 15 +-----------------------------------------------------------------------+ 16 | Author: Thomas Bruederli <roundcube@gmail.com> | 17 +-----------------------------------------------------------------------+ 18*/ 19 20class rcmail_action_contacts_index extends rcmail_action 21{ 22 public static $aliases = [ 23 'add' => 'edit', 24 ]; 25 26 protected static $SEARCH_MODS_DEFAULT = [ 27 'name' => 1, 28 'firstname' => 1, 29 'surname' => 1, 30 'email' => 1, 31 '*' => 1, 32 ]; 33 34 /** 35 * General definition of contact coltypes 36 */ 37 public static $CONTACT_COLTYPES = [ 38 'name' => [ 39 'size' => 40, 40 'maxlength' => 50, 41 'limit' => 1, 42 'label' => 'name', 43 'category' => 'main' 44 ], 45 'firstname' => [ 46 'size' => 19, 47 'maxlength' => 50, 48 'limit' => 1, 49 'label' => 'firstname', 50 'category' => 'main' 51 ], 52 'surname' => [ 53 'size' => 19, 54 'maxlength' => 50, 55 'limit' => 1, 56 'label' => 'surname', 57 'category' => 'main' 58 ], 59 'email' => [ 60 'size' => 40, 61 'maxlength' => 254, 62 'label' => 'email', 63 'subtypes' => ['home', 'work', 'other'], 64 'category' => 'main' 65 ], 66 'middlename' => [ 67 'size' => 19, 68 'maxlength' => 50, 69 'limit' => 1, 70 'label' => 'middlename', 71 'category' => 'main' 72 ], 73 'prefix' => [ 74 'size' => 8, 75 'maxlength' => 20, 76 'limit' => 1, 77 'label' => 'nameprefix', 78 'category' => 'main' 79 ], 80 'suffix' => [ 81 'size' => 8, 82 'maxlength' => 20, 83 'limit' => 1, 84 'label' => 'namesuffix', 85 'category' => 'main' 86 ], 87 'nickname' => [ 88 'size' => 40, 89 'maxlength' => 50, 90 'limit' => 1, 91 'label' => 'nickname', 92 'category' => 'main' 93 ], 94 'jobtitle' => [ 95 'size' => 40, 96 'maxlength' => 128, 97 'limit' => 1, 98 'label' => 'jobtitle', 99 'category' => 'main' 100 ], 101 'organization' => [ 102 'size' => 40, 103 'maxlength' => 128, 104 'limit' => 1, 105 'label' => 'organization', 106 'category' => 'main' 107 ], 108 'department' => [ 109 'size' => 40, 110 'maxlength' => 128, 111 'limit' => 1, 112 'label' => 'department', 113 'category' => 'main' 114 ], 115 'gender' => [ 116 'type' => 'select', 117 'limit' => 1, 118 'label' => 'gender', 119 'category' => 'personal', 120 'options' => [ 121 'male' => 'male', 122 'female' => 'female' 123 ], 124 ], 125 'maidenname' => [ 126 'size' => 40, 127 'maxlength' => 50, 128 'limit' => 1, 129 'label' => 'maidenname', 130 'category' => 'personal' 131 ], 132 'phone' => [ 133 'size' => 40, 134 'maxlength' => 20, 135 'label' => 'phone', 136 'category' => 'main', 137 'subtypes' => ['home', 'home2', 'work', 'work2', 'mobile', 'main', 'homefax', 'workfax', 'car', 138 'pager', 'video', 'assistant', 'other'], 139 ], 140 'address' => [ 141 'type' => 'composite', 142 'label' => 'address', 143 'subtypes' => ['home', 'work', 'other'], 144 'category' => 'main', 145 'childs' => [ 146 'street' => [ 147 'label' => 'street', 148 'size' => 40, 149 'maxlength' => 50, 150 ], 151 'locality' => [ 152 'label' => 'locality', 153 'size' => 28, 154 'maxlength' => 50, 155 ], 156 'zipcode' => [ 157 'label' => 'zipcode', 158 'size' => 8, 159 'maxlength' => 15, 160 ], 161 'region' => [ 162 'label' => 'region', 163 'size' => 12, 164 'maxlength' => 50, 165 ], 166 'country' => [ 167 'label' => 'country', 168 'size' => 40, 169 'maxlength' => 50, 170 ], 171 ], 172 ], 173 'birthday' => [ 174 'type' => 'date', 175 'size' => 12, 176 'maxlength' => 16, 177 'label' => 'birthday', 178 'limit' => 1, 179 'render_func' => 'rcmail_action_contacts_index::format_date_col', 180 'category' => 'personal' 181 ], 182 'anniversary' => [ 183 'type' => 'date', 184 'size' => 12, 185 'maxlength' => 16, 186 'label' => 'anniversary', 187 'limit' => 1, 188 'render_func' => 'rcmail_action_contacts_index::format_date_col', 189 'category' => 'personal' 190 ], 191 'website' => [ 192 'size' => 40, 193 'maxlength' => 128, 194 'label' => 'website', 195 'subtypes' => ['homepage', 'work', 'blog', 'profile', 'other'], 196 'category' => 'main' 197 ], 198 'im' => [ 199 'size' => 40, 200 'maxlength' => 128, 201 'label' => 'instantmessenger', 202 'subtypes' => ['aim', 'icq', 'msn', 'yahoo', 'jabber', 'skype', 'other'], 203 'category' => 'main' 204 ], 205 'notes' => [ 206 'type' => 'textarea', 207 'size' => 40, 208 'rows' => 15, 209 'maxlength' => 500, 210 'label' => 'notes', 211 'limit' => 1 212 ], 213 'photo' => [ 214 'type' => 'image', 215 'limit' => 1, 216 'category' => 'main' 217 ], 218 'assistant' => [ 219 'size' => 40, 220 'maxlength' => 128, 221 'limit' => 1, 222 'label' => 'assistant', 223 'category' => 'personal' 224 ], 225 'manager' => [ 226 'size' => 40, 227 'maxlength' => 128, 228 'limit' => 1, 229 'label' => 'manager', 230 'category' => 'personal' 231 ], 232 'spouse' => [ 233 'size' => 40, 234 'maxlength' => 128, 235 'limit' => 1, 236 'label' => 'spouse', 237 'category' => 'personal' 238 ], 239 ]; 240 241 protected static $CONTACTS; 242 protected static $SOURCE_ID; 243 protected static $contact; 244 245 /** 246 * Request handler. 247 * 248 * @param array $args Arguments from the previous step(s) 249 */ 250 public function run($args = []) 251 { 252 $rcmail = rcmail::get_instance(); 253 254 // Prepare coltypes 255 foreach (self::$CONTACT_COLTYPES as $idx => $val) { 256 if (!empty($val['label'])) { 257 self::$CONTACT_COLTYPES[$idx]['label'] = $rcmail->gettext($val['label']); 258 } 259 if (!empty($val['options'])) { 260 foreach ($val['options'] as $i => $v) { 261 self::$CONTACT_COLTYPES[$idx]['options'][$i] = $rcmail->gettext($v); 262 } 263 } 264 if (!empty($val['childs'])) { 265 foreach ($val['childs'] as $i => $v) { 266 self::$CONTACT_COLTYPES[$idx]['childs'][$i]['label'] = $rcmail->gettext($v['label']); 267 if (empty($v['type'])) { 268 self::$CONTACT_COLTYPES[$idx]['childs'][$i]['type'] = 'text'; 269 } 270 } 271 } 272 if (empty($val['type'])) { 273 self::$CONTACT_COLTYPES[$idx]['type'] = 'text'; 274 } 275 } 276 277 // Addressbook UI 278 if (!$rcmail->action && !$rcmail->output->ajax_call) { 279 // add list of address sources to client env 280 $js_list = $rcmail->get_address_sources(); 281 282 // count all/writeable sources 283 $writeable = 0; 284 $count = 0; 285 286 foreach ($js_list as $sid => $s) { 287 $count++; 288 if (!$s['readonly']) { 289 $writeable++; 290 } 291 // unset hidden sources 292 if (!empty($s['hidden'])) { 293 unset($js_list[$sid]); 294 } 295 } 296 297 $rcmail->output->set_env('display_next', (bool) $rcmail->config->get('display_next')); 298 $rcmail->output->set_env('search_mods', $rcmail->config->get('addressbook_search_mods', self::$SEARCH_MODS_DEFAULT)); 299 $rcmail->output->set_env('address_sources', $js_list); 300 $rcmail->output->set_env('writable_source', $writeable); 301 $rcmail->output->set_env('contact_move_enabled', $writeable > 1); 302 $rcmail->output->set_env('contact_copy_enabled', $writeable > 1 || ($writeable == 1 && count($js_list) > 1)); 303 304 $rcmail->output->set_pagetitle($rcmail->gettext('contacts')); 305 306 $_SESSION['addressbooks_count'] = $count; 307 $_SESSION['addressbooks_count_writeable'] = $writeable; 308 309 // select address book 310 $source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); 311 312 // use first directory by default 313 if (!strlen($source) || !isset($js_list[$source])) { 314 $source = $rcmail->config->get('default_addressbook'); 315 if (!strlen($source) || !isset($js_list[$source])) { 316 $source = strval(key($js_list)); 317 } 318 } 319 320 self::$CONTACTS = self::contact_source($source, true); 321 } 322 323 // remove undo information... 324 if (!empty($_SESSION['contact_undo'])) { 325 // ...after timeout 326 $undo = $_SESSION['contact_undo']; 327 $undo_time = $rcmail->config->get('undo_timeout', 0); 328 if ($undo['ts'] < time() - $undo_time) { 329 $rcmail->session->remove('contact_undo'); 330 } 331 } 332 333 // register UI objects 334 $rcmail->output->add_handlers([ 335 'directorylist' => [$this, 'directory_list'], 336 'savedsearchlist' => [$this, 'savedsearch_list'], 337 'addresslist' => [$this, 'contacts_list'], 338 'addresslisttitle' => [$this, 'contacts_list_title'], 339 'recordscountdisplay' => [$this, 'rowcount_display'], 340 'searchform' => [$rcmail->output, 'search_form'] 341 ]); 342 343 // Disable qr-code if php-gd or Endroid's QrCode is not installed 344 if (!$rcmail->output->ajax_call) { 345 $rcmail->output->set_env('qrcode', function_exists('imagecreate') && class_exists('Endroid\QrCode\QrCode')); 346 $rcmail->output->add_label('qrcode'); 347 } 348 } 349 350 // instantiate a contacts object according to the given source 351 public static function contact_source($source = null, $init_env = false, $writable = false) 352 { 353 if (!strlen($source)) { 354 $source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); 355 } 356 357 $rcmail = rcmail::get_instance(); 358 $page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50)); 359 360 // Get object 361 $contacts = $rcmail->get_address_book($source, $writable); 362 363 if (!$contacts) { 364 return null; 365 } 366 367 $contacts->set_pagesize($page_size); 368 369 // set list properties and session vars 370 if (!empty($_GET['_page'])) { 371 $contacts->set_page(($_SESSION['page'] = intval($_GET['_page']))); 372 } 373 else { 374 $contacts->set_page(isset($_SESSION['page']) ? $_SESSION['page'] : 1); 375 } 376 377 if ($group = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_GP)) { 378 $contacts->set_group($group); 379 } 380 381 if (!$init_env) { 382 return $contacts; 383 } 384 385 $rcmail->output->set_env('readonly', $contacts->readonly); 386 $rcmail->output->set_env('source', (string) $source); 387 $rcmail->output->set_env('group', $group); 388 389 // reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object 390 if (is_array($contacts->coltypes)) { 391 // remove cols not listed by the backend class 392 $contact_cols = isset($contacts->coltypes[0]) ? array_flip($contacts->coltypes) : $contacts->coltypes; 393 self::$CONTACT_COLTYPES = array_intersect_key(self::$CONTACT_COLTYPES, $contact_cols); 394 395 // add associative coltypes definition 396 if (empty($contacts->coltypes[0])) { 397 foreach ($contacts->coltypes as $col => $colprop) { 398 if (!empty($colprop['childs'])) { 399 foreach ($colprop['childs'] as $childcol => $childprop) { 400 $colprop['childs'][$childcol] = array_merge((array) self::$CONTACT_COLTYPES[$col]['childs'][$childcol], $childprop); 401 } 402 } 403 404 if (isset(self::$CONTACT_COLTYPES[$col])) { 405 self::$CONTACT_COLTYPES[$col] = array_merge(self::$CONTACT_COLTYPES[$col], $colprop); 406 } 407 else { 408 self::$CONTACT_COLTYPES[$col] = $colprop; 409 } 410 } 411 } 412 } 413 414 $rcmail->output->set_env('photocol', !empty(self::$CONTACT_COLTYPES['photo'])); 415 416 return $contacts; 417 } 418 419 public static function set_sourcename($abook) 420 { 421 $rcmail = rcmail::get_instance(); 422 423 // get address book name (for display) 424 if ($abook && !empty($_SESSION['addressbooks_count']) && $_SESSION['addressbooks_count'] > 1) { 425 $name = $abook->get_name(); 426 if (!$name) { 427 $name = $rcmail->gettext('personaladrbook'); 428 } 429 430 $rcmail->output->set_env('sourcename', html_entity_decode($name, ENT_COMPAT, 'UTF-8')); 431 } 432 } 433 434 public static function directory_list($attrib) 435 { 436 437 if (empty($attrib['id'])) { 438 $attrib['id'] = 'rcmdirectorylist'; 439 } 440 441 $rcmail = rcmail::get_instance(); 442 $out = ''; 443 $jsdata = []; 444 445 $line_templ = html::tag('li', 446 ['id' => 'rcmli%s', 'class' => '%s', 'noclose' => true], 447 html::a( 448 [ 449 'href' => '%s', 450 'rel' => '%s', 451 'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('list','%s',this)" 452 ], 453 '%s' 454 ) 455 ); 456 457 $sources = (array) $rcmail->output->get_env('address_sources'); 458 reset($sources); 459 460 // currently selected source 461 $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); 462 463 foreach ($sources as $j => $source) { 464 $id = strval(strlen($source['id']) ? $source['id'] : $j); 465 $js_id = rcube::JQ($id); 466 467 // set class name(s) 468 $class_name = 'addressbook'; 469 if ($current === $id) { 470 $class_name .= ' selected'; 471 } 472 if (!empty($source['readonly'])) { 473 $class_name .= ' readonly'; 474 } 475 if (!empty($source['class_name'])) { 476 $class_name .= ' ' . $source['class_name']; 477 } 478 479 $name = $source['name'] ?: $id; 480 $out .= sprintf($line_templ, 481 rcube_utils::html_identifier($id, true), 482 $class_name, 483 rcube::Q($rcmail->url(['_source' => $id])), 484 $source['id'], 485 $js_id, 486 $name 487 ); 488 489 $groupdata = ['out' => $out, 'jsdata' => $jsdata, 'source' => $id]; 490 if (!empty($source['groups'])) { 491 $groupdata = self::contact_groups($groupdata); 492 } 493 $jsdata = $groupdata['jsdata']; 494 $out = $groupdata['out']; 495 $out .= '</li>'; 496 } 497 498 $rcmail->output->set_env('contactgroups', $jsdata); 499 $rcmail->output->set_env('collapsed_abooks', (string) $rcmail->config->get('collapsed_abooks','')); 500 $rcmail->output->add_gui_object('folderlist', $attrib['id']); 501 $rcmail->output->include_script('treelist.js'); 502 503 // add some labels to client 504 $rcmail->output->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember', 505 'newgroup', 'grouprename', 'searchsave', 'namex', 'save', 'import', 'importcontacts', 506 'advsearch', 'search' 507 ); 508 509 return html::tag('ul', $attrib, $out, html::$common_attrib); 510 } 511 512 public static function savedsearch_list($attrib) 513 { 514 if (empty($attrib['id'])) { 515 $attrib['id'] = 'rcmsavedsearchlist'; 516 } 517 518 $rcmail = rcmail::get_instance(); 519 $out = ''; 520 $line_templ = html::tag('li', 521 ['id' => 'rcmli%s', 'class' => '%s'], 522 html::a([ 523 'href' => '#', 524 'rel' => 'S%s', 525 'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('listsearch', '%s', this)" 526 ], 527 '%s' 528 ) 529 ); 530 531 // Saved searches 532 $sources = $rcmail->user->list_searches(rcube_user::SEARCH_ADDRESSBOOK); 533 foreach ($sources as $source) { 534 $id = $source['id']; 535 $js_id = rcube::JQ($id); 536 537 // set class name(s) 538 $classes = ['contactsearch']; 539 if (!empty($source['class_name'])) { 540 $classes[] = $source['class_name']; 541 } 542 543 $out .= sprintf($line_templ, 544 rcube_utils::html_identifier('S' . $id, true), 545 join(' ', $classes), 546 $id, 547 $js_id, 548 rcube::Q($source['name'] ?: $id) 549 ); 550 } 551 552 $rcmail->output->add_gui_object('savedsearchlist', $attrib['id']); 553 554 return html::tag('ul', $attrib, $out, html::$common_attrib); 555 } 556 557 public static function contact_groups($args) 558 { 559 $rcmail = rcmail::get_instance(); 560 $groups = $rcmail->get_address_book($args['source'])->list_groups(); 561 $groups_html = ''; 562 563 if (!empty($groups)) { 564 $line_templ = html::tag('li', 565 ['id' => 'rcmli%s', 'class' => 'contactgroup'], 566 html::a([ 567 'href' => '#', 568 'rel' => '%s:%s', 569 'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)" 570 ], 571 '%s' 572 ) 573 ); 574 575 // append collapse/expand toggle and open a new <ul> 576 $is_collapsed = strpos($rcmail->config->get('collapsed_abooks',''), '&'.rawurlencode($args['source']).'&') !== false; 577 $args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); 578 579 foreach ($groups as $group) { 580 $groups_html .= sprintf($line_templ, 581 rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true), 582 $args['source'], 583 $group['ID'], 584 $args['source'], 585 $group['ID'], 586 rcube::Q($group['name']) 587 ); 588 589 $args['jsdata']['G' . $args['source'] . $group['ID']] = [ 590 'source' => $args['source'], 591 'id' => $group['ID'], 592 'name' => $group['name'], 593 'type' => 'group' 594 ]; 595 } 596 } 597 598 $style = !empty($is_collapsed) || empty($groups) ? 'display:none;' : null; 599 600 $args['out'] .= html::tag('ul', ['class' => 'groups', 'style' => $style], $groups_html); 601 602 return $args; 603 } 604 605 // return the contacts list as HTML table 606 public static function contacts_list($attrib) 607 { 608 $rcmail = rcmail::get_instance(); 609 610 // define list of cols to be displayed 611 $a_show_cols = ['name', 'action']; 612 613 // add id to message list table if not specified 614 if (empty($attrib['id'])) { 615 $attrib['id'] = 'rcmAddressList'; 616 } 617 618 // create XHTML table 619 $out = self::table_output($attrib, [], $a_show_cols, self::$CONTACTS->primary_key); 620 621 // set client env 622 $rcmail->output->add_gui_object('contactslist', $attrib['id']); 623 $rcmail->output->set_env('current_page', (int) self::$CONTACTS->list_page); 624 $rcmail->output->include_script('list.js'); 625 626 // add some labels to client 627 $rcmail->output->add_label('deletecontactconfirm', 'copyingcontact', 'movingcontact', 'contactdeleting'); 628 629 return $out; 630 } 631 632 public static function js_contacts_list($result, $prefix = '') 633 { 634 if (empty($result) || $result->count == 0) { 635 return; 636 } 637 638 $rcmail = rcmail::get_instance(); 639 640 // define list of cols to be displayed 641 $a_show_cols = ['name', 'action']; 642 643 while ($row = $result->next()) { 644 $emails = rcube_addressbook::get_col_values('email', $row, true); 645 $row['CID'] = $row['ID']; 646 $row['email'] = reset($emails); 647 $source_id = $rcmail->output->get_env('source'); 648 $a_row_cols = []; 649 $type = !empty($row['_type']) ? $row['_type'] : 'person'; 650 $classes = [$type]; 651 652 // build contact ID with source ID 653 if (isset($row['sourceid'])) { 654 $row['ID'] = $row['ID'].'-'.$row['sourceid']; 655 $source_id = $row['sourceid']; 656 } 657 658 // format each col 659 foreach ($a_show_cols as $col) { 660 $val = null; 661 switch ($col) { 662 case 'name': 663 $val = rcube::Q(rcube_addressbook::compose_list_name($row)); 664 break; 665 666 case 'action': 667 if ($type == 'group') { 668 $val = html::a([ 669 'href' => '#list', 670 'rel' => $row['ID'], 671 'title' => $rcmail->gettext('listgroup'), 672 'onclick' => sprintf( 673 "return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", 674 rcmail_output::JS_OBJECT_NAME, 675 $source_id, 676 $row['CID'] 677 ), 678 'class' => 'pushgroup', 679 'data-action-link' => true, 680 ], 681 '»' 682 ); 683 } 684 else { 685 $val = null; 686 } 687 break; 688 689 default: 690 $val = rcube::Q($row[$col]); 691 break; 692 } 693 694 if ($val !== null) { 695 $a_row_cols[$col] = $val; 696 } 697 } 698 699 if (!empty($row['readonly'])) { 700 $classes[] = 'readonly'; 701 } 702 703 $rcmail->output->command($prefix . 'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes), 704 array_intersect_key($row, ['ID' => 1,'readonly' => 1, '_type' => 1, 'email' => 1,'name' => 1]) 705 ); 706 } 707 } 708 709 public static function contacts_list_title($attrib) 710 { 711 $rcmail = rcmail::get_instance(); 712 $attrib += ['label' => 'contacts', 'id' => 'rcmabooklisttitle', 'tag' => 'span']; 713 unset($attrib['name']); 714 715 $rcmail->output->add_gui_object('addresslist_title', $attrib['id']); 716 $rcmail->output->add_label('contacts','uponelevel'); 717 718 return html::tag($attrib['tag'], $attrib, $rcmail->gettext($attrib['label']), html::$common_attrib); 719 } 720 721 public static function rowcount_display($attrib) 722 { 723 $rcmail = rcmail::get_instance(); 724 725 if (empty($attrib['id'])) { 726 $attrib['id'] = 'rcmcountdisplay'; 727 } 728 729 $rcmail->output->add_gui_object('countdisplay', $attrib['id']); 730 731 if (!empty($attrib['label'])) { 732 $_SESSION['contactcountdisplay'] = $attrib['label']; 733 } 734 735 return html::span($attrib, $rcmail->gettext('loading')); 736 } 737 738 public static function get_rowcount_text($result = null) 739 { 740 $rcmail = rcmail::get_instance(); 741 742 // read nr of contacts 743 if (empty($result) && !empty(self::$CONTACTS)) { 744 $result = self::$CONTACTS->get_result(); 745 } 746 747 if (empty($result) || $result->count == 0) { 748 return $rcmail->gettext('nocontactsfound'); 749 } 750 751 $page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50)); 752 753 return $rcmail->gettext([ 754 'name' => !empty($_SESSION['contactcountdisplay']) ? $_SESSION['contactcountdisplay'] : 'contactsfromto', 755 'vars' => [ 756 'from' => $result->first + 1, 757 'to' => min($result->count, $result->first + $page_size), 758 'count' => $result->count 759 ] 760 ]); 761 } 762 763 public static function get_type_label($type) 764 { 765 $rcmail = rcmail::get_instance(); 766 $label = 'type' . $type; 767 768 if ($rcmail->text_exists($label, '*', $domain)) { 769 return $rcmail->gettext($label, $domain); 770 } 771 772 if ( 773 preg_match('/\w+(\d+)$/', $label, $m) 774 && ($label = preg_replace('/(\d+)$/', '', $label)) 775 && $rcmail->text_exists($label, '*', $domain) 776 ) { 777 return $rcmail->gettext($label, $domain) . ' ' . $m[1]; 778 } 779 780 return ucfirst($type); 781 } 782 783 public static function contact_form($form, $record, $attrib = null) 784 { 785 $rcmail = rcmail::get_instance(); 786 787 // group fields 788 $head_fields = [ 789 'source' => ['source'], 790 'names' => ['prefix','firstname','middlename','surname','suffix'], 791 'displayname' => ['name'], 792 'nickname' => ['nickname'], 793 'organization' => ['organization'], 794 'department' => ['department'], 795 'jobtitle' => ['jobtitle'], 796 ]; 797 798 // Allow plugins to modify contact form content 799 $plugin = $rcmail->plugins->exec_hook('contact_form', [ 800 'form' => $form, 801 'record' => $record, 802 'head_fields' => $head_fields 803 ]); 804 805 $form = $plugin['form']; 806 $record = $plugin['record']; 807 $head_fields = $plugin['head_fields']; 808 $edit_mode = $rcmail->action != 'show' && $rcmail->action != 'print'; 809 $compact = self::get_bool_attr($attrib, 'compact-form'); 810 $use_labels = self::get_bool_attr($attrib, 'use-labels'); 811 $with_source = self::get_bool_attr($attrib, 'with-source'); 812 $out = ''; 813 814 if (!empty($attrib['deleteicon'])) { 815 $del_button = html::img([ 816 'src' => $rcmail->output->get_skin_file($attrib['deleteicon']), 817 'alt' => $rcmail->gettext('delete') 818 ]); 819 } 820 else { 821 $del_button = html::span('inner', $rcmail->gettext('delete')); 822 } 823 824 unset($attrib['deleteicon']); 825 826 // get default coltypes 827 $coltypes = self::$CONTACT_COLTYPES; 828 $coltype_labels = []; 829 $business_mode = $rcmail->config->get('contact_form_mode') === 'business'; 830 831 foreach ($coltypes as $col => $prop) { 832 if (!empty($prop['subtypes'])) { 833 // re-order subtypes, so 'work' is before 'home' 834 if ($business_mode) { 835 $work_opts = array_filter($prop['subtypes'], function($var) { return strpos($var, 'work') !== false; }); 836 if (!empty($work_opts)) { 837 $coltypes[$col]['subtypes'] = $prop['subtypes'] = array_merge( 838 $work_opts, 839 array_diff($prop['subtypes'], $work_opts) 840 ); 841 } 842 } 843 844 $subtype_names = array_map('rcmail_action_contacts_index::get_type_label', $prop['subtypes']); 845 $select_subtype = new html_select([ 846 'name' => "_subtype_{$col}[]", 847 'class' => 'contactselectsubtype custom-select', 848 'title' => $prop['label'] . ' ' . $rcmail->gettext('type') 849 ]); 850 $select_subtype->add($subtype_names, $prop['subtypes']); 851 852 $coltypes[$col]['subtypes_select'] = $select_subtype->show(); 853 } 854 855 if (!empty($prop['childs'])) { 856 foreach ($prop['childs'] as $childcol => $cp) { 857 $coltype_labels[$childcol] = ['label' => $cp['label']]; 858 } 859 } 860 } 861 862 foreach ($form as $section => $fieldset) { 863 // skip empty sections 864 if (empty($fieldset['content'])) { 865 continue; 866 } 867 868 $select_add = new html_select([ 869 'class' => 'addfieldmenu custom-select', 870 'rel' => $section, 871 'data-compact' => $compact ? "true" : null 872 ]); 873 $select_add->_count = 0; 874 $select_add->add($rcmail->gettext('addfield'), ''); 875 876 // render head section with name fields (not a regular list of rows) 877 if ($section == 'head') { 878 $content = ''; 879 880 // unset display name if it is composed from name parts 881 $dname = rcube_addressbook::compose_display_name(['name' => ''] + (array) $record); 882 if (isset($record['name']) && $record['name'] == $dname) { 883 unset($record['name']); 884 } 885 886 foreach ($head_fields as $blockname => $colnames) { 887 $fields = ''; 888 $block_attr = ['class' => $blockname . (count($colnames) == 1 ? ' row' : '')]; 889 890 foreach ($colnames as $col) { 891 if ($col == 'source') { 892 if (!$with_source || !($source = $rcmail->output->get_env('sourcename'))) { 893 continue; 894 } 895 896 if (!$edit_mode) { 897 $record['source'] = $rcmail->gettext('addressbook') . ': ' . $source; 898 } 899 else if ($rcmail->action == 'add') { 900 $record['source'] = $source; 901 } 902 else { 903 continue; 904 } 905 } 906 // skip cols unknown to the backend 907 else if (empty($coltypes[$col])) { 908 continue; 909 } 910 911 // skip cols not listed in the form definition 912 if (is_array($fieldset['content']) && !in_array($col, array_keys($fieldset['content']))) { 913 continue; 914 } 915 916 // only string values are expected here 917 if (isset($record[$col]) && is_array($record[$col])) { 918 $record[$col] = join(' ', $record[$col]); 919 } 920 921 if (!$edit_mode) { 922 if (!empty($record[$col])) { 923 $fields .= html::span('namefield ' . $col, rcube::Q($record[$col])) . ' '; 924 } 925 } 926 else { 927 $visible = true; 928 $colprop = []; 929 930 if (!empty($fieldset['content'][$col])) { 931 $colprop += (array) $fieldset['content'][$col]; 932 } 933 934 if (!empty($coltypes[$col])) { 935 $colprop += (array) $coltypes[$col]; 936 } 937 938 if (empty($colprop['id'])) { 939 $colprop['id'] = 'ff_' . $col; 940 } 941 942 if (empty($record[$col]) && empty($colprop['visible'])) { 943 $visible = false; 944 $colprop['style'] = $use_labels ? null : 'display:none'; 945 $select_add->add($colprop['label'], $col); 946 } 947 948 if ($col == 'source') { 949 $input = self::source_selector(['id' => $colprop['id']]); 950 } 951 else { 952 $val = isset($record[$col]) ? $record[$col] : null; 953 $input = rcube_output::get_edit_field($col, $val, $colprop); 954 } 955 956 if ($use_labels) { 957 $_content = html::label($colprop['id'], rcube::Q($colprop['label'])) . html::div(null, $input); 958 if (count($colnames) > 1) { 959 $fields .= html::div(['class' => 'row', 'style' => $visible ? null : 'display:none'], $_content); 960 } 961 else { 962 $fields .= $_content; 963 $block_attr['style'] = $visible ? null : 'display:none'; 964 } 965 } 966 else { 967 $fields .= $input; 968 } 969 } 970 } 971 972 if ($fields) { 973 $content .= html::div($block_attr, $fields); 974 } 975 } 976 977 if ($edit_mode) { 978 $content .= html::p('addfield', $select_add->show(null)); 979 } 980 981 $legend = !empty($fieldset['name']) ? html::tag('legend', null, rcube::Q($fieldset['name'])) : ''; 982 $out .= html::tag('fieldset', $attrib, $legend . $content, html::$common_attrib) ."\n"; 983 continue; 984 } 985 986 $content = ''; 987 if (is_array($fieldset['content'])) { 988 foreach ($fieldset['content'] as $col => $colprop) { 989 // remove subtype part of col name 990 $tokens = explode(':', $col); 991 $field = $tokens[0]; 992 993 if (empty($tokens[1])) { 994 $subtype = $business_mode ? 'work' : 'home'; 995 } 996 else { 997 $subtype = $tokens[1]; 998 } 999 1000 // skip cols unknown to the backend 1001 if (empty($coltypes[$field]) && empty($colprop['value'])) { 1002 continue; 1003 } 1004 1005 // merge colprop with global coltype configuration 1006 if (!empty($coltypes[$field])) { 1007 $colprop += $coltypes[$field]; 1008 } 1009 1010 if (!isset($colprop['type'])) { 1011 $colprop['type'] = 'text'; 1012 } 1013 1014 $label = isset($colprop['label']) ? $colprop['label'] : $rcmail->gettext($col); 1015 1016 // prepare subtype selector in edit mode 1017 if ($edit_mode && isset($colprop['subtypes']) && is_array($colprop['subtypes'])) { 1018 $subtype_names = array_map('rcmail_action_contacts_index::get_type_label', $colprop['subtypes']); 1019 $select_subtype = new html_select([ 1020 'name' => "_subtype_{$col}[]", 1021 'class' => 'contactselectsubtype custom-select', 1022 'title' => $colprop['label'] . ' ' . $rcmail->gettext('type') 1023 ]); 1024 $select_subtype->add($subtype_names, $colprop['subtypes']); 1025 } 1026 else { 1027 $select_subtype = null; 1028 } 1029 1030 $rows = ''; 1031 1032 list($values, $subtypes) = self::contact_field_values($record, "$field:$subtype", $colprop); 1033 1034 foreach ($values as $i => $val) { 1035 if (!empty($subtypes[$i])) { 1036 $subtype = $subtypes[$i]; 1037 } 1038 1039 $fc = isset($coltypes[$field]['count']) ? intval($coltypes[$field]['count']) : 0; 1040 $colprop['id'] = 'ff_' . $col . $fc; 1041 $row_class = 'row'; 1042 1043 // render composite field 1044 if ($colprop['type'] == 'composite') { 1045 $row_class .= ' composite'; 1046 $composite = []; 1047 $template = $rcmail->config->get($col . '_template', '{'.join('} {', array_keys($colprop['childs'])).'}'); 1048 $j = 0; 1049 1050 foreach ($colprop['childs'] as $childcol => $cp) { 1051 if (!empty($val) && is_array($val)) { 1052 if (!empty($val[$childcol])) { 1053 $childvalue = $val[$childcol]; 1054 } 1055 else { 1056 $childvalue = isset($val[$j]) ? $val[$j] : null; 1057 } 1058 } 1059 else { 1060 $childvalue = ''; 1061 } 1062 1063 if ($edit_mode) { 1064 if (!empty($colprop['subtypes']) || $colprop['limit'] != 1) { 1065 $cp['array'] = true; 1066 } 1067 1068 $cp_type = isset($cp['type']) ? $cp['type'] : null; 1069 $composite['{'.$childcol.'}'] = rcube_output::get_edit_field($childcol, $childvalue, $cp, $cp_type) . ' '; 1070 } 1071 else { 1072 if (!empty($cp['render_func'])) { 1073 $childval = call_user_func($cp['render_func'], $childvalue, $childcol); 1074 } 1075 else { 1076 $childval = rcube::Q($childvalue); 1077 } 1078 1079 $composite['{' . $childcol . '}'] = html::span('data ' . $childcol, $childval) . ' '; 1080 } 1081 1082 $j++; 1083 } 1084 1085 $coltypes[$field] += (array) $colprop; 1086 1087 if (isset($coltypes[$field]['count'])) { 1088 $coltypes[$field]['count']++; 1089 } 1090 else { 1091 $coltypes[$field]['count'] = 1; 1092 } 1093 1094 $val = preg_replace('/\{\w+\}/', '', strtr($template, $composite)); 1095 1096 if ($compact) { 1097 $val = html::div('content', str_replace('<br/>', '', $val)); 1098 } 1099 } 1100 else if ($edit_mode) { 1101 // call callback to render/format value 1102 if (!empty($colprop['render_func'])) { 1103 $val = call_user_func($colprop['render_func'], $val, $col); 1104 } 1105 1106 $coltypes[$field] = (array) $colprop + $coltypes[$field]; 1107 1108 if (!empty($colprop['subtypes']) || $colprop['limit'] != 1) { 1109 $colprop['array'] = true; 1110 } 1111 1112 // load jquery UI datepicker for date fields 1113 if (isset($colprop['type']) && $colprop['type'] == 'date') { 1114 $colprop['class'] = (!empty($colprop['class']) ? $colprop['class'] . ' ' : '') . 'datepicker'; 1115 if (empty($colprop['render_func'])) { 1116 $val = self::format_date_col($val); 1117 } 1118 } 1119 1120 $val = rcube_output::get_edit_field($col, $val, $colprop, $colprop['type']); 1121 1122 if (empty($coltypes[$field]['count'])) { 1123 $coltypes[$field]['count'] = 1; 1124 } 1125 else { 1126 $coltypes[$field]['count']++; 1127 } 1128 } 1129 else if (!empty($colprop['render_func'])) { 1130 $val = call_user_func($colprop['render_func'], $val, $col); 1131 } 1132 else if (isset($colprop['options']) && isset($colprop['options'][$val])) { 1133 $val = $colprop['options'][$val]; 1134 } 1135 else { 1136 $val = rcube::Q($val); 1137 } 1138 1139 // use subtype as label 1140 if (!empty($colprop['subtypes'])) { 1141 $label = self::get_type_label($subtype); 1142 } 1143 1144 $_del_btn = html::a([ 1145 'href' => '#del', 1146 'class' => 'contactfieldbutton deletebutton', 1147 'title' => $rcmail->gettext('delete'), 1148 'rel' => $col 1149 ], 1150 $del_button 1151 ); 1152 1153 // add delete button/link 1154 if (!$compact && $edit_mode 1155 && (empty($colprop['visible']) || empty($colprop['limit']) || $colprop['limit'] > 1) 1156 ) { 1157 $val .= $_del_btn; 1158 } 1159 1160 // display row with label 1161 if ($label) { 1162 if ($rcmail->action == 'print') { 1163 $_label = rcube::Q($colprop['label'] . ($label != $colprop['label'] ? ' (' . $label . ')' : '')); 1164 if (!$compact) { 1165 $_label = html::div('contactfieldlabel label', $_label); 1166 } 1167 } 1168 else if ($select_subtype) { 1169 $_label = $select_subtype->show($subtype); 1170 if (!$compact) { 1171 $_label = html::div('contactfieldlabel label', $_label); 1172 } 1173 } 1174 else { 1175 $_label = html::label(['class' => 'contactfieldlabel label', 'for' => $colprop['id']], rcube::Q($label)); 1176 } 1177 1178 if (!$compact) { 1179 $val = html::div('contactfieldcontent ' . $colprop['type'], $val); 1180 } 1181 else { 1182 $val .= $_del_btn; 1183 } 1184 1185 $rows .= html::div($row_class, $_label . $val); 1186 } 1187 // row without label 1188 else { 1189 $rows .= html::div($row_class, $compact ? $val : html::div('contactfield', $val)); 1190 } 1191 } 1192 1193 // add option to the add-field menu 1194 if (empty($colprop['limit']) || empty($coltypes[$field]['count']) || $coltypes[$field]['count'] < $colprop['limit']) { 1195 $select_add->add($colprop['label'], $col); 1196 $select_add->_count++; 1197 } 1198 1199 // wrap rows in fieldgroup container 1200 if ($rows) { 1201 $c_class = 'contactfieldgroup ' 1202 . (!empty($colprop['subtypes']) ? 'contactfieldgroupmulti ' : '') 1203 . 'contactcontroller' . $col; 1204 $with_label = !empty($colprop['subtypes']) && $rcmail->action != 'print'; 1205 $content .= html::tag( 1206 'fieldset', 1207 ['class' => $c_class], 1208 ($with_label ? html::tag('legend', null, rcube::Q($colprop['label'])) : ' ') . $rows 1209 ); 1210 } 1211 } 1212 1213 if (!$content && (!$edit_mode || !$select_add->_count)) { 1214 continue; 1215 } 1216 1217 // also render add-field selector 1218 if ($edit_mode) { 1219 $content .= html::p('addfield', $select_add->show(null, ['style' => $select_add->_count ? null : 'display:none'])); 1220 } 1221 1222 $content = html::div(['id' => 'contactsection' . $section], $content); 1223 } 1224 else { 1225 $content = $fieldset['content']; 1226 } 1227 1228 if ($content) { 1229 $fattribs = !empty($attrib['fieldset-class']) ? ['class' => $attrib['fieldset-class']] : null; 1230 $fcontent = html::tag('legend', null, rcube::Q($fieldset['name'])) . $content; 1231 $out .= html::tag('fieldset', $fattribs, $fcontent) . "\n"; 1232 } 1233 } 1234 1235 if ($edit_mode) { 1236 $rcmail->output->set_env('coltypes', $coltypes + $coltype_labels); 1237 $rcmail->output->set_env('delbutton', $del_button); 1238 $rcmail->output->add_label('delete'); 1239 } 1240 1241 return $out; 1242 } 1243 1244 public static function contact_field_values($record, $field_name, $colprop) 1245 { 1246 list($field, $subtype) = explode(':', $field_name); 1247 1248 $subtypes = []; 1249 $values = []; 1250 1251 if (!empty($colprop['value'])) { 1252 $values = (array) $colprop['value']; 1253 } 1254 else if (!empty($colprop['subtypes'])) { 1255 // iterate over possible subtypes and collect values with their subtype 1256 $c_values = rcube_addressbook::get_col_values($field, $record); 1257 1258 foreach ($colprop['subtypes'] as $st) { 1259 if (isset($c_values[$st])) { 1260 foreach ((array) $c_values[$st] as $value) { 1261 $i = count($values); 1262 $subtypes[$i] = $st; 1263 $values[$i] = $value; 1264 } 1265 1266 $c_values[$st] = null; 1267 } 1268 } 1269 1270 // TODO: add $st to $select_subtype if missing ? 1271 foreach ($c_values as $st => $vals) { 1272 foreach ((array) $vals as $value) { 1273 $i = count($values); 1274 $subtypes[$i] = $st; 1275 $values[$i] = $value; 1276 } 1277 } 1278 } 1279 else if (isset($record[$field_name])) { 1280 $values = $record[$field_name]; 1281 } 1282 else if (isset($record[$field])) { 1283 $values = $record[$field]; 1284 } 1285 1286 // hack: create empty values array to force this field to be displayed 1287 if (empty($values) && !empty($colprop['visible'])) { 1288 $values = ['']; 1289 } 1290 1291 if (!is_array($values)) { 1292 // $values can be an object, don't use (array)$values syntax 1293 $values = !empty($values) ? [$values] : []; 1294 } 1295 1296 return [$values, $subtypes]; 1297 } 1298 1299 public static function contact_photo($attrib) 1300 { 1301 if ($result = self::$CONTACTS->get_result()) { 1302 $record = $result->first(); 1303 } 1304 else { 1305 $record = ['photo' => null, '_type' => 'contact']; 1306 } 1307 1308 $rcmail = rcmail::get_instance(); 1309 1310 if (!empty($record['_type']) && $record['_type'] == 'group' && !empty($attrib['placeholdergroup'])) { 1311 $photo_img = $rcmail->output->abs_url($attrib['placeholdergroup'], true); 1312 $photo_img = $rcmail->output->asset_url($photo_img); 1313 } 1314 elseif (!empty($attrib['placeholder'])) { 1315 $photo_img = $rcmail->output->abs_url($attrib['placeholder'], true); 1316 $photo_img = $rcmail->output->asset_url($photo_img); 1317 } 1318 else { 1319 $photo_img = 'data:image/gif;base64,' . rcmail_output::BLANK_GIF; 1320 } 1321 1322 1323 $rcmail->output->set_env('photo_placeholder', $photo_img); 1324 1325 unset($attrib['placeholder']); 1326 1327 $plugin = $rcmail->plugins->exec_hook('contact_photo', [ 1328 'record' => $record, 1329 'data' => isset($record['photo']) ? $record['photo'] : null 1330 ]); 1331 1332 // check if we have photo data from contact form 1333 if (!empty(self::$contact)) { 1334 if (!empty(self::$contact['photo'])) { 1335 if (self::$contact['photo'] == '-del-') { 1336 $record['photo'] = ''; 1337 } 1338 else if ($_SESSION['contacts']['files'][self::$contact['photo']]) { 1339 $record['photo'] = $file_id = self::$contact['photo']; 1340 } 1341 } 1342 } 1343 1344 $ff_value = ''; 1345 1346 if (!empty($plugin['url'])) { 1347 $photo_img = $plugin['url']; 1348 } 1349 else if (!empty($record['photo']) && preg_match('!^https?://!i', $record['photo'])) { 1350 $photo_img = $record['photo']; 1351 } 1352 else if (!empty($record['photo'])) { 1353 $url = ['_action' => 'photo', '_cid' => $record['ID'], '_source' => self::$SOURCE_ID]; 1354 if (!empty($file_id)) { 1355 $url['_photo'] = $ff_value = $file_id; 1356 } 1357 $photo_img = $rcmail->url($url); 1358 } 1359 else { 1360 $ff_value = '-del-'; // will disable delete-photo action 1361 } 1362 1363 $content = html::div($attrib, html::img([ 1364 'src' => $photo_img, 1365 'alt' => $rcmail->gettext('contactphoto'), 1366 'onerror' => 'this.onerror = null; this.src = rcmail.env.photo_placeholder;', 1367 ])); 1368 1369 if (!empty(self::$CONTACT_COLTYPES['photo']) && ($rcmail->action == 'edit' || $rcmail->action == 'add')) { 1370 $rcmail->output->add_gui_object('contactphoto', $attrib['id']); 1371 $hidden = new html_hiddenfield(['name' => '_photo', 'id' => 'ff_photo', 'value' => $ff_value]); 1372 $content .= $hidden->show(); 1373 } 1374 1375 return $content; 1376 } 1377 1378 public static function format_date_col($val) 1379 { 1380 $rcmail = rcmail::get_instance(); 1381 return $rcmail->format_date($val, $rcmail->config->get('date_format', 'Y-m-d'), false); 1382 } 1383 1384 /** 1385 * Updates saved search after data changed 1386 */ 1387 public static function search_update($return = false) 1388 { 1389 $rcmail = rcmail::get_instance(); 1390 1391 if (empty($_REQUEST['_search'])) { 1392 return false; 1393 } 1394 1395 $search_request = $_REQUEST['_search']; 1396 1397 if (!isset($_SESSION['contact_search'][$search_request])) { 1398 return false; 1399 } 1400 1401 $search = (array) $_SESSION['contact_search'][$search_request]; 1402 $sort_col = $rcmail->config->get('addressbook_sort_col', 'name'); 1403 $afields = $return ? $rcmail->config->get('contactlist_fields') : ['name', 'email']; 1404 $records = []; 1405 1406 foreach ($search as $s => $set) { 1407 $source = $rcmail->get_address_book($s); 1408 1409 // reset page 1410 $source->set_page(1); 1411 $source->set_pagesize(9999); 1412 $source->set_search_set($set); 1413 1414 // get records 1415 $result = $source->list_records($afields); 1416 1417 if (!$result->count) { 1418 unset($search[$s]); 1419 continue; 1420 } 1421 1422 if ($return) { 1423 while ($row = $result->next()) { 1424 $row['sourceid'] = $s; 1425 $key = rcube_addressbook::compose_contact_key($row, $sort_col); 1426 $records[$key] = $row; 1427 } 1428 unset($result); 1429 } 1430 1431 $search[$s] = $source->get_search_set(); 1432 } 1433 1434 $_SESSION['contact_search'][$search_request] = $search; 1435 1436 return $records; 1437 } 1438 1439 /** 1440 * Returns contact ID(s) and source(s) from GET/POST data 1441 * 1442 * @param string $filter Return contact identifier for this specific source 1443 * @param int $request_type Type of the input var (rcube_utils::INPUT_*) 1444 * 1445 * @return array List of contact IDs per-source 1446 */ 1447 public static function get_cids($filter = null, $request_type = rcube_utils::INPUT_GPC) 1448 { 1449 // contact ID (or comma-separated list of IDs) is provided in two 1450 // forms. If _source is an empty string then the ID is a string 1451 // containing contact ID and source name in form: <ID>-<SOURCE> 1452 1453 $cid = rcube_utils::get_input_value('_cid', $request_type); 1454 $source = (string) rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); 1455 1456 if (is_array($cid)) { 1457 return $cid; 1458 } 1459 1460 if (!preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid)) { 1461 return []; 1462 } 1463 1464 $cid = explode(',', $cid); 1465 $got_source = strlen($source); 1466 $result = []; 1467 1468 // create per-source contact IDs array 1469 foreach ($cid as $id) { 1470 // extract source ID from contact ID (it's there in search mode) 1471 // see #1488959 and #1488862 for reference 1472 if (!$got_source) { 1473 if ($sep = strrpos($id, '-')) { 1474 $contact_id = substr($id, 0, $sep); 1475 $source_id = (string) substr($id, $sep+1); 1476 if (strlen($source_id)) { 1477 $result[$source_id][] = $contact_id; 1478 } 1479 } 1480 } 1481 else { 1482 if (substr($id, -($got_source+1)) === "-$source") { 1483 $id = substr($id, 0, -($got_source+1)); 1484 } 1485 $result[$source][] = $id; 1486 } 1487 } 1488 1489 return $filter !== null ? $result[$filter] : $result; 1490 } 1491 1492 /** 1493 * Returns HTML code for an addressbook selector 1494 * 1495 * @param array $attrib Template object attributes 1496 * 1497 * @return string HTML code of a <select> element, or <span> if there's only one writeable source 1498 */ 1499 public static function source_selector($attrib) 1500 { 1501 $rcmail = rcmail::get_instance(); 1502 $sources_list = $rcmail->get_address_sources(true, true); 1503 1504 if (count($sources_list) < 2) { 1505 $source = $sources_list[self::$SOURCE_ID]; 1506 $hiddenfield = new html_hiddenfield(['name' => '_source', 'value' => self::$SOURCE_ID]); 1507 1508 return html::span($attrib, $source['name'] . $hiddenfield->show()); 1509 } 1510 1511 $attrib['name'] = '_source'; 1512 $attrib['is_escaped'] = true; 1513 $attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . ".command('save', 'reload', this.form)"; 1514 1515 $select = new html_select($attrib); 1516 1517 foreach ($sources_list as $source) { 1518 $select->add($source['name'], $source['id']); 1519 } 1520 1521 return $select->show(self::$SOURCE_ID); 1522 } 1523} 1524