1<?php 2/** 3 * EGroupware - Addressbook - user interface 4 * 5 * @link www.egroupware.org 6 * @author Cornelius Weiss <egw@von-und-zu-weiss.de> 7 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> 8 * @copyright (c) 2005-19 by Ralf Becker <RalfBecker-AT-outdoor-training.de> 9 * @copyright (c) 2005/6 by Cornelius Weiss <egw@von-und-zu-weiss.de> 10 * @package addressbook 11 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 12 */ 13 14use EGroupware\Api; 15use EGroupware\Api\Link; 16use EGroupware\Api\Framework; 17use EGroupware\Api\Egw; 18use EGroupware\Api\Acl; 19use EGroupware\Api\Vfs; 20use EGroupware\Api\Etemplate; 21 22/** 23 * General user interface object of the adressbook 24 */ 25class addressbook_ui extends addressbook_bo 26{ 27 public $public_functions = array( 28 'search' => True, 29 'edit' => True, 30 'view' => True, 31 'index' => True, 32 'photo' => True, 33 'emailpopup'=> True, 34 'migrate2ldap' => True, 35 'admin_set_fileas' => True, 36 'admin_set_all_cleanup' => True, 37 'cat_add' => True, 38 ); 39 protected $org_views; 40 41 /** 42 * Addressbook configuration (stored as phpgwapi = general server config) 43 * 44 * @var array 45 */ 46 protected $config; 47 48 /** 49 * Fields to copy, default if nothing specified in config 50 * 51 * @var array 52 */ 53 static public $copy_fields = array( 54 'org_name', 55 'org_unit', 56 'adr_one_street', 57 'adr_one_street2', 58 'adr_one_locality', 59 'adr_one_region', 60 'adr_one_postalcode', 61 'adr_one_countryname', 62 'adr_one_countrycode', 63 'email', 64 'url', 65 'tel_work', 66 'cat_id' 67 ); 68 69 /** 70 * Instance of eTemplate class 71 * 72 * @var Etemplate 73 */ 74 protected $tmpl; 75 76 /** 77 * @var array 78 */ 79 public $grouped_views; 80 81 /** 82 * Constructor 83 * 84 * @param string $contact_app 85 */ 86 function __construct($contact_app='addressbook') 87 { 88 parent::__construct($contact_app); 89 90 $this->tmpl = new Etemplate(); 91 92 $this->grouped_views = array( 93 'org_name' => lang('Organisations'), 94 'org_name,adr_one_locality' => lang('Organisations by location'), 95 'org_name,org_unit' => lang('Organisations by departments'), 96 'duplicates' => lang('Duplicates'), 97 'shared_by_me' => lang('Shared by me'), 98 ); 99 100 // make sure the hook for export_limit is registered 101 if (!Api\Hooks::exists('export_limit','addressbook')) Api\Hooks::read(true); 102 103 $this->config =& $GLOBALS['egw_info']['server']; 104 105 // check if a contact specific export limit is set, if yes use it also for etemplate's csv export 106 $this->config['export_limit'] = $this->config['contact_export_limit'] = Api\Storage\Merge::getExportLimit($app='addressbook'); 107 108 if ($this->config['copy_fields'] && ($fields = is_array($this->config['copy_fields']) ? 109 $this->config['copy_fields'] : unserialize($this->config['copy_fields']))) 110 { 111 // Set country code if country name is selected 112 $supported_fields = $this->get_fields('supported',null,0); 113 if(in_array('adr_one_countrycode', $supported_fields) && in_array('adr_one_countryname',$fields)) 114 { 115 $fields[] = 'adr_one_countrycode'; 116 } 117 if(in_array('adr_two_countrycode', $supported_fields) && in_array('adr_two_countryname',$fields)) 118 { 119 $fields[] = 'adr_two_countrycode'; 120 } 121 122 self::$copy_fields = $fields; 123 } 124 } 125 126 /** 127 * List contacts of an addressbook 128 * 129 * @param array $_content =null submitted content 130 * @param string $msg =null message to show 131 */ 132 function index($_content=null,$msg=null) 133 { 134 //echo "<p>uicontacts::index(".print_r($_content,true).",'$msg')</p>\n"; 135 if (($re_submit = is_array($_content))) 136 { 137 138 if (isset($_content['nm']['rows']['delete'])) // handle a single delete like delete with the checkboxes 139 { 140 $id = @key($_content['nm']['rows']['delete']); 141 $_content['nm']['action'] = 'delete'; 142 $_content['nm']['selected'] = array($id); 143 } 144 if (isset($_content['nm']['rows']['document'])) // handle insert in default document button like an action 145 { 146 $id = @key($_content['nm']['rows']['document']); 147 $_content['nm']['action'] = 'document'; 148 $_content['nm']['selected'] = array($id); 149 } 150 if ($_content['nm']['action'] !== '' && $_content['nm']['action'] !== null) 151 { 152 if (!count($_content['nm']['selected']) && !$_content['nm']['select_all'] && $_content['nm']['action'] != 'delete_list') 153 { 154 $msg = lang('You need to select some contacts first'); 155 } 156 elseif ($_content['nm']['action'] == 'view_org' || $_content['nm']['action'] == 'view_duplicates') 157 { 158 // grouped view via context menu 159 $_content['nm']['grouped_view'] = array_shift($_content['nm']['selected']); 160 } 161 else 162 { 163 $success = $failed = $action_msg = null; 164 if ($this->action($_content['nm']['action'],$_content['nm']['selected'],$_content['nm']['select_all'], 165 $success,$failed,$action_msg,'index',$msg,$_content['nm']['checkboxes'], $error_msg)) 166 { 167 $msg .= lang('%1 contact(s) %2',$success,$action_msg); 168 Framework::message($msg); 169 } 170 elseif(is_null($msg)) 171 { 172 if (empty($error_msg)) 173 { 174 $msg .= lang('%1 contact(s) %2, %3 failed because of insufficent rights !!!', $success, $action_msg, $failed); 175 } 176 else 177 { 178 $msg .= lang('%1 contact(s) %2, %3 failed because of %4 !!!', $success, $action_msg, $failed, $error_msg); 179 } 180 Framework::message($msg,'error'); 181 } 182 $msg = ''; 183 } 184 } 185 if ($_content['nm']['rows']['infolog']) 186 { 187 $org = key($_content['nm']['rows']['infolog']); 188 return $this->infolog_org_view($org); 189 } 190 if ($_content['nm']['rows']['view']) // show all contacts of an organisation 191 { 192 $grouped_view = key($_content['nm']['rows']['view']); 193 } 194 else 195 { 196 $grouped_view = $_content['nm']['grouped_view']; 197 } 198 $typeselection = $_content['nm']['col_filter']['tid']; 199 } 200 elseif($_GET['add_list']) 201 { 202 $list = $this->add_list($_GET['add_list'],$_GET['owner']?$_GET['owner']:$this->user); 203 if ($list === true) 204 { 205 $msg = lang('List already exists!'); 206 } 207 elseif ($list) 208 { 209 $msg = lang('List created'); 210 } 211 else 212 { 213 $msg = lang('List creation failed, no rights!'); 214 } 215 } 216 $preserv = array(); 217 $content = array(); 218 if($msg || $_GET['msg']) 219 { 220 Framework::message($msg ? $msg : $_GET['msg']); 221 } 222 223 $content['nm'] = Api\Cache::getSession('addressbook', 'index'); 224 if (!is_array($content['nm'])) 225 { 226 $content['nm'] = array( 227 'get_rows' => 'addressbook.addressbook_ui.get_rows', // I method/callback to request the data for the rows eg. 'notes.bo.get_rows' 228 'bottom_too' => false, // I show the nextmatch-line (arrows, filters, search, ...) again after the rows 229 'never_hide' => True, // I never hide the nextmatch-line if less then maxmatch entrie 230 'start' => 0, // IO position in list 231 'cat_id' => '', // IO category, if not 'no_cat' => True 232 'search' => '', // IO search pattern 233 'order' => 'n_family', // IO name of the column to sort after (optional for the sortheaders) 234 'sort' => 'ASC', // IO direction of the sort: 'ASC' or 'DESC' 235 'col_filter' => array(), // IO array of column-name value pairs (optional for the filterheaders) 236 //'cat_id_label' => lang('Categories'), 237 //'filter_label' => lang('Addressbook'), // I label for filter (optional) 238 'filter' => '', // =All // IO filter, if not 'no_filter' => True 239 'filter_no_lang' => True, // I set no_lang for filter (=dont translate the options) 240 'no_filter2' => True, // I disable the 2. filter (params are the same as for filter) 241 //'filter2_label'=> lang('Distribution lists'), // IO filter2, if not 'no_filter2' => True 242 'filter2' => '', // IO filter2, if not 'no_filter2' => True 243 'filter2_no_lang'=> True, // I set no_lang for filter2 (=dont translate the options) 244 'lettersearch' => true, 245 // using a positiv list now, as we constantly adding new columns in addressbook, but not removing them from default 246 'default_cols' => 'type,n_fileas_n_given_n_family_n_family_n_given_org_name_n_family_n_given_n_fileas,'. 247 'number,org_name,org_unit,'. 248 'business_adr_one_countrycode_adr_one_postalcode,tel_work_tel_cell_tel_home,url_email_email_home', 249 /* old negative list 250 'default_cols' => '!cat_id,contact_created_contact_modified,distribution_list,contact_id,owner,room',*/ 251 'filter2_onchange' => "return app.addressbook.filter2_onchange();", 252 'filter2_tags' => true, 253 //'actions' => $this->get_actions(), // set on each request, as it depends on some filters 254 'row_id' => 'id', 255 'row_modified' => 'modified', 256 'is_parent' => 'group_count', 257 'parent_id' => 'parent_id', 258 'favorites' => true, 259 ); 260 261 // use the state of the last session stored in the user prefs 262 if (($state = @unserialize($this->prefs['index_state']))) 263 { 264 $content['nm'] = array_merge($content['nm'],$state); 265 } 266 } 267 $sel_options['cat_id'] = array('' => lang('All categories'), '0' => lang('None')); 268 269 $content['nm']['placeholder_actions'] = array('add'); 270 // Edit and delete list actions depends on permissions 271 if($this->get_lists(Acl::EDIT)) 272 { 273 $content['nm']['placeholder_actions'][] = 'rename_list'; 274 $content['nm']['placeholder_actions'][] = 'delete_list'; 275 } 276 277 // Search parameter passed in 278 if ($_GET['search']) { 279 $content['nm']['search'] = $_GET['search']; 280 } 281 if (isset($typeselection)) $content['nm']['col_filter']['tid'] = $typeselection; 282 // save the tid for use in creating new addressbook entrys via UI. Current tid is to be used as type of new entrys 283 //error_log(__METHOD__.__LINE__.' '.$content['nm']['col_filter']['tid']); 284 Api\Cache::setSession('addressbook','active_tid',$content['nm']['col_filter']['tid']); 285 if ($this->lists_available()) 286 { 287 $sel_options['filter2'] = $this->get_lists(Acl::READ,array('' => lang('No distribution list'))); 288 $sel_options['filter2']['add'] = lang('Add a new list').'...'; // put it at the end 289 } 290 291 // Organisation stuff is not (yet) availible with ldap 292 if($GLOBALS['egw_info']['server']['contact_repository'] != 'ldap') 293 { 294 $content['nm']['header_left'] = 'addressbook.index.left'; 295 } 296 $sel_options['filter'] = $sel_options['owner'] = $this->get_addressbooks(Acl::READ, lang('All addressbooks')); 297 $sel_options['to'] = array( 298 'to' => 'To', 299 'cc' => 'Cc', 300 'bcc' => 'Bcc', 301 ); 302 $sel_options['adr_one_countrycode']['-custom-'] = lang('No country selected'); 303 304 // if there is any export limit set, pass it on to the nextmatch, to be evaluated by the export 305 if (isset($this->config['contact_export_limit']) && (int)$this->config['contact_export_limit']) $content['nm']['export_limit']=$this->config['contact_export_limit']; 306 307 // Merge to email dialog needs the infolog types 308 $infolog = new infolog_bo(); 309 $sel_options['info_type'] = $infolog->enums['type']; 310 311 // dont show tid-selection if we have only one content_type 312 // be a bit more sophisticated about it 313 $availabletypes = array_keys($this->content_types); 314 if ($content['nm']['col_filter']['tid'] && !in_array($content['nm']['col_filter']['tid'],$availabletypes)) 315 { 316 //_debug_array(array('Typefilter:'=> $content['nm']['col_filter']['tid'],'Available Types:'=>$availabletypes,'action:'=>'remove invalid filter')); 317 unset($content['nm']['col_filter']['tid']); 318 } 319 if (!isset($content['nm']['col_filter']['tid'])) $content['nm']['col_filter']['tid'] = $availabletypes[0]; 320 if (count($this->content_types) > 1) 321 { 322 foreach($this->content_types as $tid => $data) 323 { 324 $sel_options['tid'][$tid] = $data['name']; 325 } 326 } 327 else 328 { 329 $this->tmpl->disableElement('nm[col_filter][tid]'); 330 } 331 // get the availible grouped-views plus the label of the contacts view of one group 332 $sel_options['grouped_view'] = $this->grouped_views; 333 if (isset($grouped_view)) 334 { 335 $content['nm']['grouped_view'] = $grouped_view; 336 } 337 338 $content['nm']['actions'] = $this->get_actions($content['nm']['col_filter']['tid']); 339 340 if (!isset($sel_options['grouped_view'][(string) $content['nm']['grouped_view']])) 341 { 342 $sel_options['grouped_view'] += $this->_get_grouped_name((string)$content['nm']['grouped_view']); 343 } 344 // unset the filters regarding grouped views, when there is no group selected 345 if (empty($sel_options['grouped_view'][(string) $content['nm']['grouped_view']]) || stripos($grouped_view,":") === false ) 346 { 347 $this->unset_grouped_filters($content['nm']); 348 } 349 $content['nm']['grouped_view_label'] = $sel_options['grouped_view'][(string) $content['nm']['grouped_view']]; 350 351 // allow to also filter by (not) shared contacts 352 $sel_options['shared_with'] = [ 353 'not' => lang('not shared'), 354 'shared' => lang('shared'), 355 ]; 356 357 $this->tmpl->read('addressbook.index'); 358 return $this->tmpl->exec('addressbook.addressbook_ui.index', 359 $content,$sel_options,array(),$preserv); 360 } 361 362 /** 363 * Get actions / context menu items 364 * 365 * @param ?string $tid_filter =null 366 * @return array see Etemplate\Widget\Nextmatch::get_actions() 367 */ 368 public function get_actions($tid_filter=null) 369 { 370 // Contact view 371 $actions = array( 372 'view' => array( 373 'caption' => 'CRM-View', 374 'default' => $GLOBALS['egw_info']['user']['preferences']['addressbook']['crm_list'] != '~edit~', 375 'allowOnMultiple' => false, 376 'group' => $group=1, 377 'onExecute' => 'javaScript:app.addressbook.view', 378 'enableClass' => 'contact_contact', 379 'hideOnDisabled' => true, 380 // Children added below 381 'children' => array(), 382 'hideOnMobile' => true 383 ), 384 'open' => array( 385 'caption' => 'Open', 386 'default' => $GLOBALS['egw_info']['user']['preferences']['addressbook']['crm_list'] == '~edit~', 387 'allowOnMultiple' => false, 388 'enableClass' => 'contact_contact', 389 'hideOnDisabled' => true, 390 'url' => 'menuaction=addressbook.addressbook_ui.edit&contact_id=$id', 391 'popup' => Link::get_registry('addressbook', 'edit_popup'), 392 'group' => $group, 393 ), 394 'add' => array( 395 'caption' => 'Add', 396 'group' => $group, 397 'hideOnDisabled' => true, 398 'children' => array( 399 'new' => array( 400 'caption' => 'New', 401 'url' => 'menuaction=addressbook.addressbook_ui.edit', 402 'popup' => Link::get_registry('addressbook', 'add_popup'), 403 'icon' => 'new', 404 ), 405 'copy' => array( 406 'caption' => 'Copy', 407 'url' => 'menuaction=addressbook.addressbook_ui.edit&makecp=1&contact_id=$id', 408 'popup' => Link::get_registry('addressbook', 'add_popup'), 409 'enableClass' => 'contact_contact', 410 'allowOnMultiple' => false, 411 'icon' => 'copy', 412 ), 413 ), 414 'hideOnMobile' => true 415 ), 416 ); 417 // CRM view options 418 $crm_apps = array('infolog','tracker'); 419 foreach($crm_apps as $crm_index => $app) 420 { 421 if (!$GLOBALS['egw_info']['user']['apps'][$app]) 422 { 423 unset($crm_apps[$crm_index]); 424 } 425 } 426 if($GLOBALS['egw_info']['user']['apps']['infolog']) 427 { 428 array_splice($crm_apps, 1, 0, 'infolog-organisation'); 429 } 430 if(count($crm_apps) > 1) 431 { 432 foreach($crm_apps as $app) 433 { 434 $actions['view']['children']["view-$app"] = array( 435 'caption' => $app, 436 'icon' => "$app/navbar" 437 ); 438 } 439 } 440 441 // org view 442 $actions += array( 443 'view_org' => array( 444 'caption' => 'View', 445 'default' => true, 446 'allowOnMultiple' => false, 447 'group' => $group=1, 448 'enableClass' => 'contact_organisation', 449 'hideOnDisabled' => true 450 ), 451 'add_org' => array( 452 'caption' => 'Add', 453 'group' => $group, 454 'allowOnMultiple' => false, 455 'enableClass' => 'contact_organisation', 456 'hideOnDisabled' => true, 457 'url' => 'menuaction=addressbook.addressbook_ui.edit&org=$id', 458 'popup' => Link::get_registry('addressbook', 'add_popup'), 459 ), 460 ); 461 462 // Duplicates view 463 $actions += array( 464 'view_duplicates' => array( 465 'caption' => 'View', 466 'default' => true, 467 'allowOnMultiple' => false, 468 'group' => $group=1, 469 'enableClass' => 'contact_duplicate', 470 'hideOnDisabled' => true 471 ) 472 ); 473 474 ++$group; // other AB related stuff group: lists, AB's, categories 475 // categories submenu 476 $actions['cat'] = array( 477 'caption' => 'Categories', 478 'group' => $group, 479 'children' => array( 480 'cat_add' => Etemplate\Widget\Nextmatch::category_action( 481 'addressbook',$group,'Add category', 'cat_add_', 482 true, 0,Etemplate\Widget\Nextmatch::DEFAULT_MAX_MENU_LENGTH,false 483 )+array( 484 'icon' => 'foldertree_nolines_plus', 485 'disableClass' => 'rowNoEdit', 486 ), 487 'cat_del' => Etemplate\Widget\Nextmatch::category_action( 488 'addressbook',$group,'Delete category', 'cat_del_', 489 true, 0,Etemplate\Widget\Nextmatch::DEFAULT_MAX_MENU_LENGTH,false 490 )+array( 491 'icon' => 'foldertree_nolines_minus', 492 'disableClass' => 'rowNoEdit', 493 ), 494 ), 495 ); 496 if (!$GLOBALS['egw_info']['user']['apps']['preferences']) unset($actions['cats']['children']['cat_edit']); 497 // Submenu for all distributionlist stuff 498 $actions['lists'] = array( 499 'caption' => 'Distribution lists', 500 'children' => array( 501 'list_add' => array( 502 'caption' => 'Add a new list', 503 'icon' => 'new', 504 'onExecute' => 'javaScript:app.addressbook.add_new_list', 505 ), 506 ), 507 'group' => $group, 508 ); 509 if (($add_lists = $this->get_lists(Acl::EDIT))) // do we have distribution lists?, and are we allowed to edit them 510 { 511 $actions['lists']['children'] += array( 512 'to_list' => array( 513 'caption' => 'Add to distribution list', 514 'children' => $add_lists, 515 'prefix' => 'to_list_', 516 'icon' => 'foldertree_nolines_plus', 517 'enabled' => ($add_lists?true:false), // if there are editable lists, allow to add a contact to one of them, 518 //'disableClass' => 'rowNoEdit', // wether you are allowed to edit the contact or not, as you alter a list, not the contact 519 ), 520 'remove_from_list' => array( 521 'caption' => 'Remove from distribution list', 522 'confirm' => 'Remove selected contacts from distribution list', 523 'icon' => 'foldertree_nolines_minus', 524 'enabled' => 'javaScript:app.addressbook.nm_compare_field', 525 'fieldId' => 'exec[nm][filter2]', 526 'fieldValue' => '!', // enable if list != '' 527 ), 528 'rename_list' => array( 529 'caption' => 'Rename selected distribution list', 530 'icon' => 'edit', 531 'enabled' => 'javaScript:app.addressbook.nm_compare_field', 532 'fieldId' => 'exec[nm][filter2]', 533 'fieldValue' => '!', // enable if list != '' 534 'onExecute' => 'javaScript:app.addressbook.rename_list' 535 ), 536 'delete_list' => array( 537 'caption' => 'Delete selected distribution list!', 538 'confirm' => 'Delete selected distribution list!', 539 'icon' => 'delete', 540 'enabled' => 'javaScript:app.addressbook.nm_compare_field', 541 'fieldId' => 'exec[nm][filter2]', 542 'fieldValue' => '!', // enable if list != '' 543 ), 544 ); 545 if(is_subclass_of('etemplate', 'etemplate_new')) 546 { 547 $actions['lists']['children']['remove_from_list']['fieldId'] = 'filter2'; 548 $actions['lists']['children']['rename_list']['fieldId'] = 'filter2'; 549 $actions['lists']['children']['delete_list']['fieldId'] = 'filter2'; 550 } 551 } 552 // move to AB 553 if (($move2addressbooks = $this->get_addressbooks(Acl::ADD))) // do we have addressbooks, we should 554 { 555 unset($move2addressbooks[0]); // do not offer action to move contact to an account, as we dont support that currrently 556 foreach($move2addressbooks as $owner => $label) 557 { 558 $icon = $type_label = null; 559 $this->type_icon((int)$owner, substr($owner,-1) == 'p', 'n', $icon, $type_label); 560 $move2addressbooks[$owner] = array( 561 'icon' => $icon, 562 'caption' => $label, 563 ); 564 } 565 // copy checkbox 566 $move2addressbooks= array( 567 'copy' =>array( 568 'id' => 'move_to_copy', 569 'caption' => 'Copy instead of move', 570 'checkbox' => true, 571 )) + $move2addressbooks; 572 $actions['move_to'] = array( 573 'caption' => 'Move to addressbook', 574 'children' => $move2addressbooks, 575 'prefix' => 'move_to_', 576 'group' => $group, 577 'disableClass' => 'rowNoDelete', 578 'hideOnMobile' => true 579 ); 580 } 581 if (($share2addressbooks = $this->get_addressbooks(Acl::EDIT|self::ACL_SHARED, null, null, false))) 582 { 583 unset($share2addressbooks[0]); // do not offer action to share contact into accounts AB 584 foreach ($share2addressbooks as $owner => $label) 585 { 586 if (substr($owner, -1) === 'p') // share with private AB makes no sense 587 { 588 unset($share2addressbooks[$owner]); 589 continue; 590 } 591 $icon = $type_label = null; 592 $this->type_icon((int)$owner, substr($owner, -1) == 'p', 'n', $icon, $type_label); 593 $share2addressbooks[$owner] = array( 594 'icon' => $icon, 595 'caption' => $label, 596 ); 597 } 598 $actions['shared_with'] = [ 599 'caption' => 'Share into addressbook', 600 'children' => [ 601 'writable' => [ 602 'id' => 'writable', 603 'caption' => 'Share writable', 604 'checkbox' => true, 605 ]] + $share2addressbooks + [ 606 'unshare' => [ 607 'icon' => 'delete', 608 'caption' => 'Unshare', 609 'group' => $group, 610 'enableClass' => 'unshare_contact', 611 'hideOnDisabled' => true, 612 'hideOnMobile' => true 613 ] 614 ], 615 'prefix' => 'shared_with_', 616 'group' => $group, 617 'hideOnMobile' => true 618 ]; 619 } 620 $actions['change_type'] = $this->change_type_actions($group); 621 $actions['merge'] = array( 622 'caption' => 'Merge contacts', 623 'confirm' => 'Merge into first or account, deletes all other!', 624 'hint' => 'Merge into first or account, deletes all other!', 625 'allowOnMultiple' => 'only', 626 'group' => $group, 627 'hideOnMobile' => true, 628 'enabled' => 'javaScript:app.addressbook.can_merge' 629 ); 630 // Duplicates view 631 $actions['merge_duplicates'] = array( 632 'caption' => 'Merge duplicates', 633 'group' => $group, 634 'allowOnMultiple' => true, 635 'enableClass' => 'contact_duplicate', 636 'hideOnDisabled' => true 637 ); 638 639 ++$group; // integration with other apps: infolog, calendar, filemanager, messenger 640 641 // Integrate Status Videoconference actions 642 if ($GLOBALS['egw_info']['user']['apps']['status']) 643 { 644 $actions['videoconference'] = [ 645 'caption' => 'Video Conference', 646 'icon' => 'status/videoconference', 647 'group' => $group, 648 'children' => [ 649 'call' => [ 650 'caption' => lang('Video Call'), 651 'icon' => 'status/videoconference_call', 652 'allowOnMultiple' => true, 653 'onExecute' => 'javaScript:app.addressbook.videoconference_actionCall', 654 'enabled' => 'javaScript:app.addressbook.videoconference_isUserOnline' 655 ], 656 'audiocall' => [ 657 'caption' => lang('Audio Call'), 658 'icon' => 'accept_call', 659 'allowOnMultiple' => true, 660 'onExecute' => 'javaScript:app.addressbook.videoconference_actionCall', 661 'enabled' => 'javaScript:app.addressbook.videoconference_isUserOnline' 662 ], 663 'invite' => [ 664 'caption' => lang('Invite to current call'), 665 'icon' => 'status/videoconference_join', 666 'allowOnMultiple' => true, 667 'onExecute' => 'javaScript:app.addressbook.videoconference_actionCall', 668 'enabled' => 'javaScript:app.addressbook.videoconference_isThereAnyCall' 669 ], 670 'schedule_call' => [ 671 'caption' => lang('Schedule a video conference'), 672 'icon' => 'calendar/navbar', 673 'allowOnMultiple' => true, 674 'onExecute' => 'javaScript:app.addressbook.add_cal', 675 ] 676 ] 677 ]; 678 } 679 680 if ($GLOBALS['egw_info']['user']['apps']['infolog']) 681 { 682 $actions['infolog_app'] = array( 683 'caption' => 'InfoLog', 684 'icon' => 'infolog/navbar', 685 'group' => $group, 686 'children' => array( 687 'infolog' => array( 688 'caption' => lang('View linked InfoLog entries'), 689 'icon' => 'infolog/navbar', 690 'onExecute' => 'javaScript:app.addressbook.view_infolog', 691 'disableClass' => 'contact_duplicate', 692 'allowOnMultiple' => true, 693 'hideOnDisabled' => true, 694 ), 695 'infolog_add' => array( 696 'caption' => 'Add a new Infolog', 697 'icon' => 'new', 698 'url' => 'menuaction=infolog.infolog_ui.edit&type=task&action=addressbook&action_id=$id', 699 'popup' => Link::get_registry('infolog', 'add_popup'), 700 'onExecute' => 'javaScript:app.addressbook.add_task', // call server for org-view only 701 ), 702 ), 703 'hideOnMobile' => true 704 ); 705 } 706 if ($GLOBALS['egw_info']['user']['apps']['calendar']) 707 { 708 $actions['calendar'] = array( 709 'icon' => 'calendar/navbar', 710 'caption' => 'Calendar', 711 'group' => $group, 712 'enableClass' => 'contact_contact', 713 'children' => array( 714 'calendar_view' => array( 715 'caption' => 'Show', 716 'icon' => 'view', 717 'onExecute' => 'javaScript:app.addressbook.view_calendar', 718 'targetapp' => 'calendar', // open in calendar tab, 719 'hideOnDisabled' => true, 720 ), 721 'calendar_add' => array( 722 'caption' => 'Add appointment', 723 'icon' => 'new', 724 'popup' => Link::get_registry('calendar', 'add_popup'), 725 'onExecute' => 'javaScript:app.addressbook.add_cal', 726 ), 727 ), 728 'hideOnMobile' => true 729 ); 730 } 731 //Send to email 732 $actions['email'] = array( 733 'caption' => 'Email', 734 'icon' => 'mail/navbar', 735 'enableClass' => 'contact_contact', 736 'hideOnDisabled' => true, 737 'group' => $group, 738 'children' => array( 739 'add_to_to' => array( 740 'caption' => lang('Add to To'), 741 'no_lang' => true, 742 'onExecute' => 'javaScript:app.addressbook.addEmail', 743 744 ), 745 'add_to_cc' => array( 746 'caption' => lang('Add to Cc'), 747 'no_lang' => true, 748 'onExecute' => 'javaScript:app.addressbook.addEmail', 749 750 ), 751 'add_to_bcc' => array( 752 'caption' => lang('Add to BCc'), 753 'no_lang' => true, 754 'onExecute' => 'javaScript:app.addressbook.addEmail', 755 756 ), 757 'email_business' => array( 758 'caption' => lang('Business email'), 759 'no_lang' => true, 760 'checkbox' => true, 761 'group' => $group, 762 'onExecute' => 'javaScript:app.addressbook.mailCheckbox', 763 'checked' => $this->prefs['preferredMail']['business'], 764 ), 765 'email_home' => array( 766 'caption' => lang('Home email'), 767 'no_lang' => true, 768 'checkbox' => true, 769 'group' => $group, 770 'onExecute' => 'javaScript:app.addressbook.mailCheckbox', 771 'checked' => $this->prefs['preferredMail']['private'], 772 ), 773 ), 774 775 ); 776 if (!$this->prefs['preferredMail']) 777 $actions['email']['children']['email_business']['checked'] = true; 778 779 if ($GLOBALS['egw_info']['user']['apps']['filemanager']) 780 { 781 $actions['filemanager'] = array( 782 'icon' => 'filemanager/navbar', 783 'caption' => 'Filemanager', 784 'url' => 'menuaction=filemanager.filemanager_ui.index&path=/apps/addressbook/$id&ajax=true', 785 'allowOnMultiple' => false, 786 'group' => $group, 787 // disable for for group-views, as it needs contact-ids 788 'enableClass' => 'contact_contact', 789 'hideOnMobile' => true 790 ); 791 } 792 793 $actions['geolocation'] = array( 794 'caption' => 'GeoLocation', 795 'icon' => 'map', 796 'group' => ++$group, 797 'enableClass' => 'contact_contact', 798 'children' => array ( 799 'private' => array( 800 'caption' => 'Private Address', 801 'enabled' => 'javaScript:app.addressbook.geoLocation_enabled', 802 'onExecute' => 'javaScript:app.addressbook.geoLocationExec', 803 804 ), 805 'business' => array( 806 'caption' => 'Business Address', 807 'enabled' => 'javaScript:app.addressbook.geoLocation_enabled', 808 'onExecute' => 'javaScript:app.addressbook.geoLocationExec', 809 810 ) 811 ) 812 ); 813 $actions += EGroupware\Api\Link\Sharing::get_actions('addressbook', $group); 814 815 // check if user is an admin or the export is not generally turned off (contact_export_limit is non-numerical, eg. no) 816 $exception = Api\Storage\Merge::is_export_limit_excepted(); 817 if ((isset($GLOBALS['egw_info']['user']['apps']['admin']) || $exception) || !$this->config['contact_export_limit'] || (int)$this->config['contact_export_limit']) 818 { 819 $actions['export'] = array( 820 'caption' => 'Export', 821 'icon' => 'filesave', 822 'enableClass' => 'contact_contact', 823 'group' => ++$group, 824 'children' => array( 825 'csv' => array( 826 'caption' => 'Export as CSV', 827 'allowOnMultiple' => true, 828 'url' => 'menuaction=importexport.importexport_export_ui.export_dialog&appname=addressbook&plugin=addressbook_export_contacts_csv&selection=$id&select_all=$select_all', 829 'popup' => '850x440' 830 ), 831 'vcard' => array( 832 'caption' => 'Export as VCard', 833 'postSubmit' => true, // download needs post submit (not Ajax) to work 834 'icon' => Vfs::mime_icon('text/vcard'), 835 ), 836 ), 837 'hideOnMobile' => true 838 ); 839 } 840 841 $actions['documents'] = Api\Contacts\Merge::document_action( 842 $this->prefs['document_dir'], $group, 'Insert in document', 'document_', 843 $this->prefs['default_document'], $this->config['contact_export_limit'] 844 ); 845 if ($GLOBALS['egw_info']['user']['apps']['felamimail']||$GLOBALS['egw_info']['user']['apps']['mail']) 846 { 847 $actions['mail'] = array( 848 'caption' => lang('Mail VCard'), 849 'icon' => 'filemanager/mail_post_to', 850 'group' => $group, 851 'onExecute' => 'javaScript:app.addressbook.adb_mail_vcard', 852 'enableClass' => 'contact_contact', 853 'hideOnDisabled' => true, 854 'hideOnMobile' => true, 855 'disableIfNoEPL' => true 856 ); 857 } 858 ++$group; 859 if (!($tid_filter == 'D' && !$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge')) 860 { 861 $actions['delete'] = array( 862 'caption' => 'Delete', 863 'confirm' => 'Delete this contact', 864 'confirm_multiple' => 'Delete these entries', 865 'group' => $group, 866 'disableClass' => 'rowNoDelete', 867 'onExecute' => 'javaScript:app.addressbook.action', 868 ); 869 } 870 if ($this->grants[0] & Acl::DELETE) 871 { 872 $actions['delete_account'] = array( 873 'caption' => 'Delete', 874 'icon' => 'delete', 875 'group' => $group, 876 'enableClass' => 'rowAccount', 877 'hideOnDisabled' => true, 878 'popup' => '400x200', 879 'url' => 'menuaction=admin.admin_account.delete&contact_id=$id', 880 ); 881 $actions['delete']['hideOnDisabled'] = true; 882 } 883 if($tid_filter == 'D') 884 { 885 $actions['undelete'] = array( 886 'caption' => 'Un-delete', 887 'icon' => 'revert', 888 'group' => $group, 889 'disableClass' => 'rowNoEdit', 890 ); 891 } 892 if (isset($actions['export']['children']['csv']) && 893 (!isset($GLOBALS['egw_info']['user']['apps']['importexport']) || 894 !importexport_helper_functions::has_definitions('addressbook','export'))) 895 { 896 unset($actions['export']['children']['csv']); 897 } 898 // Intercept open action in order to open entry into view mode instead of edit 899 if (Api\Header\UserAgent::mobile()) 900 { 901 $actions['open']['onExecute'] = 'javaScript:app.addressbook.viewEntry'; 902 $actions['open']['mobileViewTemplate'] = 'view?'.filemtime(Api\Etemplate\Widget\Template::rel2path('/addressbook/templates/mobile/view.xet')); 903 $actions['view']['default'] = false; 904 $actions['open']['default'] = true; 905 } 906 // Allow contacts to be dragged 907 /* 908 $actions['drag'] = array( 909 'type' => 'drag', 910 'dragType' => 'addressbook' 911 ); 912 */ 913 return $actions; 914 } 915 916 protected function change_type_actions($group) 917 { 918 919 $types = array(); 920 foreach($this->content_types as $key => $type) 921 { 922 // Skip deleted 923 if($key == self::DELETED_TYPE) continue; 924 925 $types[$key] = array( 926 'caption' => $type['name'], 927 ); 928 } 929 930 return array( 931 'caption' => 'Type', 932 'children' => $types, 933 'prefix' => 'to_type_', 934 'group' => $group, 935 'disableClass' => 'rowNoEdit', 936 'hideOnDisabled' => true, 937 'disabled' => (count($types) <= 1), 938 'hideOnMobile' => true 939 ); 940 } 941 942 /** 943 * Get a nice name for the given grouped view ID 944 * 945 * @param String $view_id Some kind of indicator for a specific group, either 946 * organisation or duplicate. It looks like key:value pairs seperated by |||. 947 * 948 * @return Array(ID => name), where ID is the $view_id passed in 949 */ 950 protected function _get_grouped_name($view_id) 951 { 952 $group_name = array(); 953 if (strpos($view_id,'*AND*')!== false) $view_id = str_replace('*AND*','&',$view_id); 954 foreach(explode('|||',$view_id) as $part) 955 { 956 list(,$name) = explode(':',$part,2); 957 if ($name) $group_name[] = $name; 958 } 959 $name = implode(': ',$group_name); 960 return $name ? array($view_id => $name) : array(); 961 } 962 963 /** 964 * Unset the relevant column filters to clear a grouped view 965 * 966 * @param Array $query 967 */ 968 protected function unset_grouped_filters(&$query) 969 { 970 unset($query['col_filter']['org_name']); 971 unset($query['col_filter']['org_unit']); 972 unset($query['col_filter']['adr_one_locality']); 973 foreach(array_keys(static::$duplicate_fields) as $field) 974 { 975 unset($query['col_filter'][$field]); 976 } 977 } 978 979 /** 980 * Adjust the query as needed and get the rows for the grouped views (organisation 981 * or duplicate contacts) 982 * 983 * @param Array $query Nextmatch query 984 * @return array rows found 985 */ 986 protected function get_grouped_rows(&$query) 987 { 988 // Query doesn't like empties 989 unset($query['col_filter']['parent_id']); 990 991 if($query['actions'] && $query['actions']['open']) 992 { 993 // Just switched from contact view, update actions 994 $query['actions'] = $this->get_actions($query['col_filter']['tid']); 995 } 996 997 $template = $query['grouped_view'] == 'duplicates' ? 'addressbook.index.duplicate_rows' : 'addressbook.index.org_rows'; 998 999 if ($query['advanced_search']) 1000 { 1001 $query['op'] = $query['advanced_search']['operator']; 1002 unset($query['advanced_search']['operator']); 1003 $query['wildcard'] = $query['advanced_search']['meth_select']; 1004 unset($query['advanced_search']['meth_select']); 1005 $original_search = $query['search']; 1006 $query['search'] = $query['advanced_search']; 1007 } 1008 1009 switch ($template) 1010 { 1011 case 'addressbook.index.org_rows': 1012 if ($query['order'] != 'org_name') 1013 { 1014 $query['sort'] = 'ASC'; 1015 $query['order'] = 'org_name'; 1016 } 1017 $query['org_view'] = $query['grouped_view']; 1018 // switch the distribution list selection off for ldap 1019 if($this->contact_repository != 'sql') 1020 { 1021 $query['no_filter2'] = true; 1022 unset($query['col_filter']['list']); // does not work here 1023 } 1024 else 1025 { 1026 $rows = parent::organisations($query); 1027 } 1028 break; 1029 case 'addressbook.index.duplicate_rows': 1030 $query['no_filter2'] = true; // switch the distribution list selection off 1031 unset($query['col_filter']['list']); // does not work for duplicates 1032 $rows = parent::duplicates($query); 1033 break; 1034 } 1035 1036 if ($query['advanced_search']) 1037 { 1038 $query['search'] = $original_search; 1039 unset($query['wildcard']); 1040 unset($query['op']); 1041 } 1042 $GLOBALS['egw_info']['flags']['params']['manual'] = array('page' => 'ManualAddressbookIndexOrga'); 1043 1044 return $rows; 1045 } 1046 1047 /** 1048 * Return the contacts in an organisation via AJAX 1049 * 1050 * @param string|string[] $org Organisation ID 1051 * @param mixed $_query Query filters (category, etc) to use, or null to use session 1052 * @return array 1053 */ 1054 public function ajax_organisation_contacts($org, $_query = null) 1055 { 1056 $org_contacts = array(); 1057 $query = !$_query ? Api\Cache::getSession('addressbook', 'index') : $_query; 1058 $query['num_rows'] = -1; // all 1059 if(!is_array($query['col_filter'])) $query['col_filter'] = array(); 1060 1061 if(!is_array($org)) $org = array($org); 1062 foreach($org as $org_name) 1063 { 1064 $query['grouped_view'] = $org_name; 1065 $checked = array(); 1066 $readonlys = null; 1067 $this->get_rows($query,$checked,$readonlys,true); // true = only return the id's 1068 if($checked[0]) 1069 { 1070 $org_contacts = array_merge($org_contacts,$checked); 1071 } 1072 } 1073 Api\Json\Response::get()->data(array_unique($org_contacts)); 1074 } 1075 1076 /** 1077 * Show the infologs of an whole organisation 1078 * 1079 * @param string $org 1080 */ 1081 function infolog_org_view($org) 1082 { 1083 $query = Api\Cache::getSession('addressbook', 'index'); 1084 $query['num_rows'] = -1; // all 1085 $query['grouped_view'] = $org; 1086 $query['searchletter'] = ''; 1087 $checked = $readonlys = null; 1088 $this->get_rows($query,$checked,$readonlys,true); // true = only return the id's 1089 1090 if (count($checked) > 1) // use a nicely formatted org-name as title in infolog 1091 { 1092 $parts = array(); 1093 if (strpos($org,'*AND*')!== false) $org = str_replace('*AND*','&',$org); 1094 foreach(explode('|||',$org) as $part) 1095 { 1096 list(,$part) = explode(':',$part,2); 1097 if ($part) $parts[] = $part; 1098 } 1099 $org = implode(', ',$parts); 1100 } 1101 else 1102 { 1103 $org = ''; // use infolog default of link-title 1104 } 1105 Egw::redirect_link('/index.php',array( 1106 'menuaction' => 'infolog.infolog_ui.index', 1107 'action' => 'addressbook', 1108 'action_id' => implode(',',$checked), 1109 'action_title' => $org, 1110 ),'infolog'); 1111 } 1112 1113 /** 1114 * Create or rename an existing email list 1115 * 1116 * @param int $list_id ID of existing list, or 0 for a new one 1117 * @param string $new_name List name 1118 * @param int $_owner List owner, or empty for current user 1119 * @param string[] [$contacts] List of contacts to add to the array 1120 * @return boolean|string 1121 */ 1122 function ajax_set_list($list_id, $new_name, $_owner = false, $contacts = array()) 1123 { 1124 // Set owner to current user, if not set 1125 $owner = $_owner ? $_owner : $GLOBALS['egw_info']['user']['account_id']; 1126 // if admin forced or set default for add_default pref 1127 // consider default_addressbook as owner which already 1128 // covered all cases in contacts class. 1129 if ($owner == (int)$GLOBALS['egw']->preferences->default['addressbook']['add_default'] || 1130 $owner == (int)$GLOBALS['egw']->preferences->forced['addressbook']['add_default']) 1131 { 1132 $owner = $this->default_addressbook; 1133 } 1134 // Check for valid list & permissions 1135 if(!(int)$list_id && !$this->check_list(null,EGW_ACL_ADD|EGW_ACL_EDIT,$owner)) 1136 { 1137 Api\Json\Response::get()->apply('egw.message', array( lang('List creation failed, no rights!'),'error')); 1138 return; 1139 } 1140 if ((int)$list_id && !$this->check_list((int)$list_id, Acl::EDIT, $owner)) 1141 { 1142 Api\Json\Response::get()->apply('egw.message', array( lang('Insufficent rights to edit this list!'),'error')); 1143 return; 1144 } 1145 1146 $list = array('list_owner' => $owner); 1147 1148 // Rename 1149 if($list_id) 1150 { 1151 $list = $this->read_list((int)$list_id); 1152 } 1153 $list['list_name'] = $new_name; 1154 1155 $new_id = $this->add_list(array('list_id' => (int)$list_id), $list['list_owner'],array(),$list); 1156 1157 if($contacts) 1158 { 1159 $this->add2list($contacts,$new_id); 1160 } 1161 Api\Json\Response::get()->apply('egw.message', array( 1162 $new_id == $list_id ? lang('Distribution list renamed') : lang('List created'), 1163 'success' 1164 )); 1165 // Success, just update selectbox to new value 1166 Api\Json\Response::get()->data($new_id == $list_id ? "true" : $new_id); 1167 } 1168 1169 /** 1170 * Ajax function to get contact data out of provided account_id 1171 * 1172 * @param string $account_id 1173 */ 1174 function ajax_get_contact ($account_id) 1175 { 1176 $bo = new Api\Contacts(); 1177 $contact = $bo->read('account:'.$account_id); 1178 Api\Json\Response::get()->data($contact); 1179 } 1180 1181 /** 1182 * Disable / clear advanced search 1183 * 1184 * Advanced search is stored server side in session no matter what the nextmatch 1185 * sends, so we have to clear it here. 1186 */ 1187 public static function ajax_clear_advanced_search() 1188 { 1189 $query = Api\Cache::getSession('addressbook', 'index'); 1190 unset($query['advanced_search']); 1191 Api\Cache::setSession('addressbook','index',$query); 1192 Api\Cache::setSession('addressbook', 'advanced_search', false); 1193 } 1194 1195 /** 1196 * Apply an action to multiple events, but called via AJAX instead of submit 1197 * 1198 * @param string $action 1199 * @param string[] $selected 1200 * @param bool $all_selected All entries are selected, not just what's in $selected 1201 * @param bool $skip_notification 1202 */ 1203 public function ajax_action($action, $selected, $all_selected, $skip_notification = false) 1204 { 1205 $success = 0; 1206 $failed = 0; 1207 $action_msg = ''; 1208 $session_name = 'index'; 1209 1210 if($this->action($action, $selected, $all_selected, $success, $failed, $action_msg, $session_name, $msg, $skip_notification)) 1211 { 1212 $msg = lang('%1 event(s) %2',$success,$action_msg); 1213 } 1214 elseif(is_null($msg)) 1215 { 1216 $msg .= lang('%1 event(s) %2, %3 failed because of insufficient rights !!!',$success,$action_msg,$failed); 1217 } 1218 Api\Json\Response::get()->message($msg); 1219 } 1220 1221 /** 1222 * apply an action to multiple contacts 1223 * 1224 * @param string/int $action 'delete', 'vcard', 'csv' or nummerical account_id to move contacts to that addessbook 1225 * @param array $checked contact id's to use if !$use_all 1226 * @param boolean $use_all if true use all contacts of the current selection (in the session) 1227 * @param int &$success number of succeded actions 1228 * @param int &$failed number of failed actions (not enought permissions) 1229 * @param string &$action_msg translated verb for the actions, to be used in a message like %1 contacts 'deleted' 1230 * @param string/array $session_name 'index' or array with session-data depending if we are in the main list or the popup 1231 * @param ?string& $error_msg on return optional error-message 1232 * @return boolean true if all actions succeded, false otherwise 1233 */ 1234 function action($action, $checked, $use_all, &$success, &$failed, &$action_msg, $session_name, &$msg, $checkboxes = NULL, &$error_msg=null) 1235 { 1236 //echo "<p>uicontacts::action('$action',".print_r($checked,true).','.(int)$use_all.",...)</p>\n"; 1237 $success = $failed = 0; 1238 $error_msg = null; 1239 if ($use_all || in_array($action,array('remove_from_list','delete_list','unshare'))) 1240 { 1241 // get the whole selection 1242 $query = is_array($session_name) ? $session_name : Api\Cache::getSession('addressbook', $session_name); 1243 1244 if ($use_all) 1245 { 1246 @set_time_limit(0); // switch off the execution time limit, as it's for big selections to small 1247 $query['num_rows'] = -1; // all 1248 $readonlys = null; 1249 $this->get_rows($query,$checked,$readonlys,true); // true = only return the id's 1250 } 1251 } 1252 // replace org_name:* id's with all id's of that org 1253 $grouped_contacts = $this->find_grouped_ids($action, $checked, $use_all, $success,$failed,$action_msg,$session_name, $msg); 1254 if ($grouped_contacts) $checked = array_unique($checked ? array_merge($checked,$grouped_contacts) : $grouped_contacts); 1255 //_debug_array($checked); exit; 1256 1257 if (substr($action,0,8) == 'move_to_') 1258 { 1259 $action = (int)substr($action,8).(substr($action,-1) == 'p' ? 'p' : ''); 1260 } 1261 elseif (substr($action,0,8) == 'to_list_') 1262 { 1263 $to_list = (int)substr($action,8); 1264 $action = 'to_list'; 1265 } 1266 elseif (substr($action, 0, 8) == 'to_type_') 1267 { 1268 $to_type = substr($action,8); 1269 $action = 'to_type'; 1270 } 1271 elseif (substr($action,0,9) == 'document_') 1272 { 1273 $document = substr($action,9); 1274 $action = 'document'; 1275 } 1276 elseif(substr($action,0,4) == 'cat_') // cat_add_123 or cat_del_456 1277 { 1278 $cat_id = (int)substr($action, 8); 1279 $action = substr($action,0,7); 1280 } 1281 elseif(substr($action, 0, 12) === 'shared_with_') 1282 { 1283 $shared_with = substr($action, 12); 1284 $action = 'shared_with'; 1285 } 1286 // Security: stop non-admins to export more then the configured number of contacts 1287 if (in_array($action,array('csv','vcard')) && $this->config['contact_export_limit'] && !Api\Storage\Merge::is_export_limit_excepted() && 1288 (!is_numeric($this->config['contact_export_limit']) || count($checked) > $this->config['contact_export_limit'])) 1289 { 1290 $action_msg = lang('exported'); 1291 $failed = count($checked); 1292 return false; 1293 } 1294 switch($action) 1295 { 1296 case 'vcard': 1297 $action_msg = lang('exported'); 1298 $vcard = new addressbook_vcal('addressbook','text/vcard'); 1299 $vcard->export($checked); 1300 // does not return! 1301 $Ok = false; 1302 break; 1303 1304 case 'merge': 1305 $error_msg = null; 1306 $success = $this->merge($checked,$error_msg); 1307 $failed = count($checked) - (int)$success; 1308 $action_msg = lang('merged'); 1309 $checked = array(); // to not start the single actions 1310 break; 1311 1312 case 'delete_list': 1313 if (!$query['filter2']) 1314 { 1315 $msg = lang('You need to select a distribution list'); 1316 } 1317 elseif($this->delete_list($query['filter2']) === false) 1318 { 1319 $msg = lang('Insufficent rights to delete this list!'); 1320 } 1321 else 1322 { 1323 $msg = lang('Distribution list deleted'); 1324 unset($query['filter2']); 1325 Api\Cache::setSession('addressbook', $session_name, $query); 1326 } 1327 return false; 1328 1329 case 'document': 1330 if (!$document) $document = $this->prefs['default_document']; 1331 $document_merge = new Api\Contacts\Merge(); 1332 $msg = $document_merge->download($document, $checked, '', $this->prefs['document_dir']); 1333 $failed = count($checked); 1334 return false; 1335 1336 case 'infolog_add': 1337 Framework::popup(Egw::link('/index.php',array( 1338 'menuaction' => 'infolog.infolog_ui.edit', 1339 'type' => 'task', 1340 'action' => 'addressbook', 1341 'action_id' => implode(',',$checked), 1342 )),'_blank',Link::get_registry('infolog', 'add_popup')); 1343 $msg = ''; // no message, as we send none in javascript too and users sees opening popup 1344 return false; 1345 1346 case 'calendar_add': // add appointment for org-views, other views are handled directly in javascript 1347 Framework::popup(Egw::link('/index.php',array( 1348 'menuaction' => 'calendar.calendar_uiforms.edit', 1349 'participants' => 'c'.implode(',c',$checked), 1350 )),'_blank',Link::get_registry('calendar', 'add_popup')); 1351 $msg = ''; // no message, as we send none in javascript too and users sees opening popup 1352 return false; 1353 1354 case 'calendar_view': // show calendar for org-views, although all views are handled directly in javascript 1355 Egw::redirect_link('/index.php',array( 1356 'menuaction' => 'calendar.calendar_uiviews.index', 1357 'owner' => 'c'.implode(',c',$checked), 1358 )); 1359 } 1360 foreach($checked as $id) 1361 { 1362 switch($action) 1363 { 1364 case 'cat_add': 1365 case 'cat_del': 1366 if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::EDIT,$contact))) 1367 { 1368 $action_msg = $action == 'cat_add' ? lang('categorie added') : lang('categorie delete'); 1369 $cat_ids = $contact['cat_id'] ? explode(',', $contact['cat_id']) : array(); //existing Api\Categories 1370 if ($action == 'cat_add') 1371 { 1372 $cat_ids[] = $cat_id; 1373 $cat_ids = array_unique($cat_ids); 1374 } 1375 elseif ((($key = array_search($cat_id,$cat_ids))) !== false) 1376 { 1377 unset($cat_ids[$key]); 1378 } 1379 $ids = $cat_ids ? implode(',',$cat_ids) : null; 1380 if ($ids !== $contact['cat_id']) 1381 { 1382 $contact['cat_id'] = $ids; 1383 $Ok = $this->save($contact); 1384 } 1385 } 1386 break; 1387 1388 case 'delete': 1389 $action_msg = lang('deleted'); 1390 if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::DELETE,$contact))) 1391 { 1392 if ($contact['owner'] || // regular contact or 1393 empty($contact['account_id']) || // accounts without account_id 1394 // already deleted account (should no longer happen, but needed to allow for cleanup) 1395 $contact['tid'] == self::DELETED_TYPE) 1396 { 1397 $Ok = $this->delete($id, $contact['tid'] != self::DELETED_TYPE && $contact['account_id']); 1398 } 1399 // delete single account --> redirect to admin 1400 elseif (count($checked) == 1 && $contact['account_id']) 1401 { 1402 Egw::redirect_link('/index.php',array( 1403 'menuaction' => 'admin.admin_account.delete', 1404 'account_id' => $contact['account_id'], 1405 )); 1406 // this does NOT return! 1407 } 1408 else // no mass delete of accounts 1409 { 1410 $Ok = false; 1411 } 1412 } 1413 break; 1414 1415 case 'undelete': 1416 $action_msg = lang('recovered'); 1417 if (($contact = $this->read($id))) 1418 { 1419 $contact['tid'] = 'n'; 1420 $Ok = $this->save($contact); 1421 } 1422 break; 1423 1424 case 'email': 1425 case 'email_home': 1426 /* this cant work anymore, as Framework::set_onload does not longer exist 1427 $action_fallback = $action == 'email' ? 'email_home' : 'email'; 1428 $action_msg = lang('added'); 1429 if(($contact = $this->read($id))) 1430 { 1431 if(strpos($contact[$action],'@') !== false) 1432 { 1433 $email = $contact[$action]; 1434 } 1435 elseif(strpos($contact[$action_fallback],'@') !== false) 1436 { 1437 $email = $contact[$action_fallback]; 1438 } 1439 else 1440 { 1441 $Ok = $email = false; 1442 } 1443 if($email) 1444 { 1445 $contact['n_fn'] = str_replace(array(',','@'),' ',$contact['n_fn']); 1446 Framework::set_onload("addEmail('".addslashes( 1447 $contact['n_fn'] ? $contact['n_fn'].' <'.trim($email).'>' : trim($email))."');"); 1448 //error_log(__METHOD__.__LINE__."addEmail('".addslashes( 1449 // $contact['n_fn'] ? $contact['n_fn'].' <'.trim($email).'>' : trim($email))."');"); 1450 $Ok = true; 1451 } 1452 }*/ 1453 break; 1454 1455 case 'remove_from_list': 1456 $action_msg = lang('removed from distribution list'); 1457 if (!$query['filter2']) 1458 { 1459 $msg = lang('You need to select a distribution list'); 1460 return false; 1461 } 1462 else 1463 { 1464 $Ok = $this->remove_from_list($id,$query['filter2']) !== false; 1465 } 1466 break; 1467 1468 case 'to_list': 1469 $action_msg = lang('added to distribution list'); 1470 if (!$to_list) 1471 { 1472 $msg = lang('You need to select a distribution list'); 1473 return false; 1474 } 1475 else 1476 { 1477 $Ok = $this->add2list($id,$to_list) !== false; 1478 } 1479 break; 1480 case 'to_type': 1481 $action_msg = lang('changed type to %1', lang($this->content_types[$to_type]['name'])); 1482 if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::EDIT,$contact))) 1483 { 1484 if (!$contact['owner']) // no change of accounts 1485 { 1486 $Ok = false; 1487 } 1488 else 1489 { 1490 $contact['tid'] = $to_type; 1491 $Ok = $this->save($contact); 1492 } 1493 } 1494 break; 1495 case 'shared_with': 1496 // as "unshare" is in "shared_with" submenu/children it uses "shared_with_unshare" 1497 if ($shared_with === 'unshare') 1498 { 1499 $action_msg = lang('unshared'); 1500 if (($Ok = !!($contact = $this->read($id)))) 1501 { 1502 $need_save = false; 1503 foreach($contact['shared'] as $key => $shared) 1504 { 1505 // only unshare contacts shared by current user 1506 if (($shared['shared_by'] == $this->user || 1507 $this->check_perms(ACL::EDIT, $contact)) && 1508 // only unshare from given addressbook, or all 1509 (empty($query['filter']) || $shared['shared_with'] == (int)$query['filter'])) 1510 { 1511 $need_save = true; 1512 unset($contact['shared'][$key]); 1513 } 1514 } 1515 // we might need to ignore acl, as we are allowed to share with just read-rights 1516 // setting user and update-time is explicitly desired for sync(-collection)! 1517 $Ok = !$need_save || $this->save($contact, true); 1518 } 1519 break; 1520 } 1521 $action_msg = lang('shared into addressbook %1', Api\Accounts::username($shared_with)); 1522 if (($Ok = !!($contact = $this->read($id)))) 1523 { 1524 $new_shared_with = [[ 1525 'shared_with' => $shared_with, 1526 'shared_by' => $this->user, 1527 'shared_at' => new Api\DateTime('now'), 1528 // only allow to share writable, if user has edit-rights! 1529 'shared_writable' => (int)($checkboxes['writable'] && $this->check_perms(Acl::EDIT, $contact)), 1530 'contact_id' => $id, 1531 'contact' => $contact, 1532 ]]; 1533 if ($this->check_shared_with($new_shared_with, $error_msg)) // returns [] if OK 1534 { 1535 $Ok = false; 1536 } 1537 else 1538 { 1539 $contact['shared'][] = $new_shared_with[0]; 1540 // we might need to ignore acl, as we are allowed to share with just read-rights 1541 // setting user and update-time is explicitly desired for sync(-collection)! 1542 $Ok = $this->save($contact, true); 1543 } 1544 } 1545 break; 1546 default: // move to an other addressbook 1547 if (!(int)$action || !($this->grants[(string) (int) $action] & Acl::EDIT)) // might be ADD in the future 1548 { 1549 return false; 1550 } 1551 if (!$checkboxes['move_to_copy']) 1552 { 1553 $action_msg = lang('moved'); 1554 if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::DELETE,$contact))) 1555 { 1556 if (!$contact['owner']) // no (mass-)move of Api\Accounts 1557 { 1558 $Ok = false; 1559 } 1560 elseif ($contact['owner'] != (int)$action || $contact['private'] != (int)(substr($action,-1) == 'p')) 1561 { 1562 $contact['owner'] = (int) $action; 1563 $contact['private'] = (int)(substr($action,-1) == 'p'); 1564 $Ok = $this->save($contact); 1565 } 1566 } 1567 } 1568 else 1569 { 1570 $action_msg = lang('copied'); 1571 if (($Ok = !!($contact = $this->read($id)) && $this->check_perms(Acl::READ,$contact))) 1572 { 1573 if ($contact['owner'] != (int)$action || $contact['private'] != (int)(substr($action,-1) == 'p')) 1574 { 1575 $this->copy_contact($contact, false); // do NOT use self::$copy_fields, copy everything but uid etc. 1576 $links = $contact['link_to']['to_id']; 1577 $contact['owner'] = (int) $action; 1578 $contact['private'] = (int)(substr($action,-1) == 'p'); 1579 $Ok = $this->save($contact); 1580 if ($Ok && is_array($links)) 1581 { 1582 Link::link('addressbook', $contact['id'], $links); 1583 } 1584 } 1585 } 1586 } 1587 break; 1588 } 1589 if ($Ok) 1590 { 1591 ++$success; 1592 } 1593 elseif ($action != 'email' && $action != 'email_home') 1594 { 1595 ++$failed; 1596 } 1597 } 1598 return !$failed; 1599 } 1600 1601 /** 1602 * Find the individual contact IDs for a list of grouped contacts 1603 * 1604 * Successful lookups are removed from the checked array. 1605 * 1606 * Used for action on organisation and duplicate views 1607 * @param string/int $action 'delete', 'vcard', 'csv' or nummerical account_id to move contacts to that addessbook 1608 * @param array $checked contact id's to use if !$use_all 1609 * @param boolean $use_all if true use all contacts of the current selection in the session (NOT used!) 1610 * @param int &$success number of succeded actions 1611 * @param int &$failed number of failed actions (not enought permissions) 1612 * @param string &$action_msg translated verb for the actions, to be used in a message like %1 contacts 'deleted' 1613 * @param string/array $session_name 'index' or 'email', or array with session-data depending if we are in the main list or the popup 1614 * 1615 * @return array List of contact IDs in the provided groups 1616 */ 1617 protected function find_grouped_ids($action,&$checked,$use_all,&$success,&$failed,&$action_msg,$session_name,&$msg) 1618 { 1619 unset($use_all); 1620 $grouped_contacts = array(); 1621 foreach((array)$checked as $n => $id) 1622 { 1623 if (substr($id,0,9) == 'org_name:' || substr($id, 0,10) == 'duplicate:') 1624 { 1625 if (count($checked) == 1 && !count($grouped_contacts) && $action == 'infolog') 1626 { 1627 return $this->infolog_org_view($id); // uses the org-name, instead of 'selected contacts' 1628 } 1629 unset($checked[$n]); 1630 $query = Api\Cache::getSession('addressbook', $session_name); 1631 $query['num_rows'] = -1; // all 1632 $query['grouped_view'] = $id; 1633 unset($query['filter2']); 1634 $extra = $readonlys = null; 1635 $this->get_rows($query,$extra,$readonlys,true); // true = only return the id's 1636 1637 // Merge them here, so we only merge the ones that are duplicates, 1638 // not merge all selected together 1639 if($action == 'merge_duplicates') 1640 { 1641 $loop_success = $loop_fail = 0; 1642 $this->action('merge', $extra, false, $loop_success, $loop_fail, $action_msg,$session_name,$msg); 1643 $success += $loop_success; 1644 $failed += $loop_fail; 1645 } 1646 if ($extra[0]) 1647 { 1648 $grouped_contacts = array_merge($grouped_contacts,$extra); 1649 } 1650 } 1651 } 1652 return $grouped_contacts; 1653 } 1654 1655 /** 1656 * Copy a given contact (not storing it!) 1657 * 1658 * Taken care only configured fields get copied and certain fields never to copy (uid etc.). 1659 * 1660 * @param array& $content 1661 * @param boolean $only_copy_fields =true true: only copy fields configured for copying (eg. no name), 1662 * false: copy everything, but never to copy fields 1663 */ 1664 function copy_contact(array &$content, $only_copy_fields=true) 1665 { 1666 $content['link_to']['to_id'] = 0; 1667 Link::link('addressbook',$content['link_to']['to_id'],'addressbook',$content['id'], 1668 lang('Copied by %1, from record #%2.',Api\Accounts::format_username('', 1669 $GLOBALS['egw_info']['user']['account_firstname'],$GLOBALS['egw_info']['user']['account_lastname']), 1670 $content['id'])); 1671 // create a new contact with the content of the old 1672 foreach(array_keys($content) as $key) 1673 { 1674 if($only_copy_fields && !in_array($key, self::$copy_fields) || in_array($key, array('id','etag','carddav_name','uid'))) 1675 { 1676 unset($content[$key]); 1677 } 1678 } 1679 if(!isset($content['owner'])) 1680 { 1681 $content['owner'] = $this->default_private ? $this->user.'p' : $this->default_addressbook; 1682 } 1683 $content['creator'] = $this->user; 1684 $content['created'] = $this->now_su; 1685 } 1686 1687 /** 1688 * rows callback for index nextmatch 1689 * 1690 * @internal 1691 * @param array &$query 1692 * @param array &$rows returned rows/cups 1693 * @param array &$readonlys eg. to disable buttons based on Acl 1694 * @param boolean $id_only =false if true only return (via $rows) an array of contact-ids, dont save state to session 1695 * @return int total number of contacts matching the selection 1696 */ 1697 function get_rows(&$query,&$rows,&$readonlys,$id_only=false) 1698 { 1699 $what = $query['sitemgr_display'] ? $query['sitemgr_display'] : 'index'; 1700 1701 if (!$id_only && !$query['csv_export']) // do NOT store state for csv_export or querying id's (no regular view) 1702 { 1703 $store_query = $query; 1704 // Do not store these 1705 foreach(array('options-cat_id','actions','action_links','placeholder_actions') as $key) 1706 { 1707 unset($store_query[$key]); 1708 } 1709 $old_state = $store_query; 1710 Api\Cache::setSession('addressbook', $what, $store_query); 1711 } 1712 else 1713 { 1714 $old_state = Api\Cache::getSession('addressbook', $what); 1715 } 1716 $GLOBALS['egw']->session->commit_session(); 1717 1718 if ($query['grouped_view'] === 'shared_by_me') 1719 { 1720 $query['col_filter']['shared_by'] = $this->user; 1721 $query['grouped_view'] = ''; 1722 } 1723 1724 if (!isset($this->grouped_views[(string) $query['grouped_view']]) || strpos($query['grouped_view'],':') === false) 1725 { 1726 // we don't have a grouped view, unset the according col_filters 1727 $this->unset_grouped_filters($query); 1728 } 1729 1730 if (isset($this->grouped_views[(string) $query['grouped_view']])) 1731 { 1732 // we have a grouped view, reset the advanced search 1733 if(!$query['search'] && $old_state['advanced_search']) $query['advanced_search'] = $old_state['advanced_search']; 1734 } 1735 elseif(!$query['search'] && array_key_exists('advanced_search',$old_state)) // eg. paging in an advanced search 1736 { 1737 $query['advanced_search'] = $old_state['advanced_search']; 1738 } 1739 1740 // Make sure old lettersearch filter doesn't stay - current letter filter will be added later 1741 foreach($query['col_filter'] as $key => $col_filter) 1742 { 1743 if(!is_numeric($key)) continue; 1744 if(preg_match('/'.$GLOBALS['egw']->db->capabilities['case_insensitive_like']. 1745 ' '.$GLOBALS['egw']->db->quote('[a-z]%').'$/i',$col_filter) == 1 1746 ) 1747 { 1748 unset($query['col_filter'][$key]); 1749 } 1750 } 1751 1752 //echo "<p>uicontacts::get_rows(".print_r($query,true).")</p>\n"; 1753 if (!$id_only) 1754 { 1755 // check if accounts are stored in ldap, which does NOT yet support the org-views 1756 if ($this->so_accounts && $query['filter'] === '0' && $query['grouped_view']) 1757 { 1758 if ($old_state['filter'] === '0') // user changed to org_view 1759 { 1760 $query['filter'] = ''; // --> change filter to all contacts 1761 } 1762 else // user changed to accounts 1763 { 1764 $query['grouped_view'] = ''; // --> change to regular contacts view 1765 } 1766 } 1767 if ($query['grouped_view'] && isset($this->grouped_views[$old_state['grouped_view']]) && !isset($this->grouped_views[$query['grouped_view']])) 1768 { 1769 $query['searchletter'] = ''; // reset lettersearch if viewing the contacts of one group (org or duplicates) 1770 } 1771 // save the state of the index in the user prefs 1772 $state = serialize(array( 1773 'filter' => $query['filter'], 1774 'cat_id' => $query['cat_id'], 1775 'order' => $query['order'], 1776 'sort' => $query['sort'], 1777 'col_filter' => array('tid' => $query['col_filter']['tid']), 1778 'grouped_view' => $query['grouped_view'], 1779 )); 1780 if ($state != $this->prefs[$what.'_state'] && !$query['csv_export']) 1781 { 1782 $GLOBALS['egw']->preferences->add('addressbook',$what.'_state',$state); 1783 // save prefs, but do NOT invalid the cache (unnecessary) 1784 $GLOBALS['egw']->preferences->save_repository(false,'user',false); 1785 } 1786 } 1787 unset($old_state); 1788 1789 if ((string)$query['cat_id'] != '') 1790 { 1791 $query['col_filter']['cat_id'] = $query['cat_id'] ? $query['cat_id'] : null; 1792 } 1793 else 1794 { 1795 unset($query['col_filter']['cat_id']); 1796 } 1797 if ($query['filter'] !== '') // not all addressbooks 1798 { 1799 $query['col_filter']['owner'] = (string) (int) $query['filter']; 1800 1801 if ($this->private_addressbook) 1802 { 1803 $query['col_filter']['private'] = substr($query['filter'],-1) == 'p' ? 1 : 0; 1804 } 1805 } 1806 else 1807 { 1808 unset($query['col_filter']['owner']); 1809 unset($query['col_filter']['private']); 1810 } 1811 if ((int)$query['filter2']) // not no distribution list 1812 { 1813 $query['col_filter']['list'] = (string) (int) $query['filter2']; 1814 } 1815 else 1816 { 1817 unset($query['col_filter']['list']); 1818 } 1819 if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] === '1') 1820 { 1821 $query['col_filter']['account_id'] = null; 1822 } 1823 else 1824 { 1825 unset($query['col_filter']['account_id']); 1826 } 1827 1828 // all backends allow now at least to use groups as distribution lists 1829 $query['no_filter2'] = false; 1830 1831 // Grouped view 1832 if (isset($this->grouped_views[(string) $query['grouped_view']]) && !$query['col_filter']['parent_id']) 1833 { 1834 $query['grouped_view_label'] = ''; 1835 $rows = $this->get_grouped_rows($query); 1836 } 1837 else // contacts view 1838 { 1839 if($query['col_filter']['parent_id']) 1840 { 1841 $query['grouped_view'] = $query['col_filter']['parent_id']; 1842 } 1843 // Query doesn't like parent_id 1844 unset($query['col_filter']['parent_id']); 1845 if ($query['grouped_view']) // view the contacts of one organisation only 1846 { 1847 if (strpos($query['grouped_view'],'*AND*') !== false) $query['grouped_view'] = str_replace('*AND*','&',$query['grouped_view']); 1848 $fields = explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']); 1849 foreach(explode('|||',$query['grouped_view']) as $part) 1850 { 1851 list($name,$value) = explode(':',$part,2); 1852 // do NOT set invalid column, as this gives an SQL error ("AND AND" in sql) 1853 if (static::$duplicate_fields[$name] && $value && ( 1854 strpos($query['grouped_view'], 'duplicate:') === 0 && in_array($name, $fields) || 1855 strpos($query['grouped_view'], 'duplicate:') !== 0 1856 )) 1857 { 1858 $query['col_filter'][$name] = $value; 1859 } 1860 } 1861 } 1862 else if($query['actions'] && !$query['actions']['edit']) 1863 { 1864 // Just switched from grouped view, update actions 1865 $query['actions'] = $this->get_actions($query['col_filter']['tid']); 1866 } 1867 // translate the select order to the really used over all 3 columns 1868 $sort = $query['sort']; 1869 switch($query['order']) // "xxx<>'' DESC" sorts contacts with empty order-criteria always at the end 1870 { // we don't exclude them, as the total would otherwise depend on the order-criteria 1871 case 'org_name': 1872 $order = "egw_addressbook.org_name<>''DESC,egw_addressbook.org_name $sort,n_family $sort,n_given $sort"; 1873 break; 1874 default: 1875 if ($query['order'][0] == '#') // we order by a custom field 1876 { 1877 $order = "{$query['order']} $sort,org_name $sort,n_family $sort,n_given $sort"; 1878 break; 1879 } 1880 $query['order'] = 'n_family'; 1881 case 'n_family': 1882 $order = "n_family<>'' DESC,n_family $sort,n_given $sort,org_name $sort"; 1883 break; 1884 case 'n_given': 1885 $order = "n_given<>'' DESC,n_given $sort,n_family $sort,org_name $sort"; 1886 break; 1887 case 'n_fileas': 1888 $order = "n_fileas<>'' DESC,n_fileas $sort"; 1889 break; 1890 case 'adr_one_postalcode': 1891 case 'adr_two_postalcode': 1892 $order = $query['order']."<>'' DESC,".$query['order']." $sort,org_name $sort,n_family $sort,n_given $sort"; 1893 break; 1894 case 'contact_modified': 1895 case 'contact_created': 1896 $order = "$query[order] IS NULL,$query[order] $sort,org_name $sort,n_family $sort,n_given $sort"; 1897 break; 1898 case 'contact_id': 1899 $order = "egw_addressbook.$query[order] $sort"; 1900 } 1901 if ($query['searchletter']) // only show contacts if the order-criteria starts with the given letter 1902 { 1903 $no_letter_search = array('adr_one_postalcode', 'adr_two_postalcode', 'contact_id', 'contact_created','contact_modified'); 1904 $query['col_filter'][] = (in_array($query['order'],$no_letter_search) ? 'org_name' : (substr($query['order'],0,1)=='#'?'':'egw_addressbook.').$query['order']).' '. 1905 $GLOBALS['egw']->db->capabilities['case_insensitive_like'].' '.$GLOBALS['egw']->db->quote($query['searchletter'].'%'); 1906 } 1907 $wildcard = '%'; 1908 $op = 'OR'; 1909 if ($query['advanced_search']) 1910 { 1911 // Make sure op & wildcard are only valid options 1912 $op = $query['advanced_search']['operator'] == $op ? $op : 'AND'; 1913 unset($query['advanced_search']['operator']); 1914 $wildcard = $query['advanced_search']['meth_select'] == $wildcard ? $wildcard : ''; 1915 unset($query['advanced_search']['meth_select']); 1916 } 1917 1918 $columsel = $this->prefs['nextmatch-addressbook.index.rows']; 1919 $columselection = $columsel ? explode(',',$columsel) : array(); 1920 $extracols = []; 1921 if (in_array('owner_shared_with', $columselection)) 1922 { 1923 $extracols[] = 'shared_with'; 1924 } 1925 1926 $rows = parent::search($query['advanced_search'] ? $query['advanced_search'] : $query['search'],$id_only, 1927 $order, $extracols, $wildcard,false, $op,[(int)$query['start'], (int)$query['num_rows']], $query['col_filter']); 1928 1929 // do we need to read the custom fields, depends on the column is enabled and customfields 1930 $available_distib_lists=$this->get_lists(Acl::READ); 1931 $ids = $calendar_participants = array(); 1932 if (!$id_only && $rows) 1933 { 1934 $show_custom_fields = (in_array('customfields',$columselection) || $this->config['index_load_cfs']) && $this->customfields; 1935 $show_calendar = $this->config['disable_event_column'] != 'True' && in_array('calendar_calendar',$columselection); 1936 $show_distributionlist = in_array('distribution_list', $columselection) || 1937 is_array($available_distib_lists) && count($available_distib_lists); 1938 if ($show_calendar || $show_custom_fields || $show_distributionlist) 1939 { 1940 foreach($rows as $val) 1941 { 1942 $ids[] = $val['id']; 1943 $calendar_participants[$val['id']] = $val['account_id'] ? $val['account_id'] : 'c'.$val['id']; 1944 } 1945 if ($show_custom_fields) 1946 { 1947 $selected_cfs = array(); 1948 if(in_array('customfields',$columselection)) 1949 { 1950 foreach($columselection as $col) 1951 { 1952 if ($col[0] == '#') $selected_cfs[] = substr($col,1); 1953 } 1954 } 1955 $selected_cfs = array_unique(array_merge($selected_cfs, (array)$this->config['index_load_cfs'])); 1956 $customfields = $this->read_customfields($ids,$selected_cfs); 1957 } 1958 if ($show_calendar && !empty($ids)) $calendar = $this->read_calendar($calendar_participants); 1959 // distributionlist memership for the entrys 1960 //_debug_array($this->get_lists(Acl::EDIT)); 1961 if ($show_distributionlist && $available_distib_lists) 1962 { 1963 $distributionlist = $this->read_distributionlist($ids,array_keys($available_distib_lists)); 1964 } 1965 } 1966 } 1967 } 1968 if (!$rows) $rows = array(); 1969 1970 if ($id_only) 1971 { 1972 foreach($rows as $n => $row) 1973 { 1974 $rows[$n] = $row['id']; 1975 } 1976 return $this->total; // no need to set other fields or $readonlys 1977 } 1978 $order = $query['order']; 1979 1980 $unshare_grants = []; 1981 foreach($this->grants as $grantee => $rights) 1982 { 1983 if ($rights & (ACL::EDIT|self::ACL_SHARED)) $unshare_grants[] = $grantee; 1984 } 1985 $readonlys = array(); 1986 foreach($rows as $n => &$row) 1987 { 1988 $given = $row['n_given'] ? $row['n_given'] : ($row['n_prefix'] ? $row['n_prefix'] : ''); 1989 1990 switch($order) 1991 { 1992 default: // postalcode, created, modified, ... 1993 case 'org_name': 1994 $row['line1'] = $row['org_name']; 1995 $row['line2'] = $row['n_family'].($given ? ', '.$given : ''); 1996 break; 1997 case 'n_family': 1998 $row['line1'] = $row['n_family'].($given ? ', '.$given : ''); 1999 $row['line2'] = $row['org_name']; 2000 break; 2001 case 'n_given': 2002 $row['line1'] = $given.' '.$row['n_family']; 2003 $row['line2'] = $row['org_name']; 2004 break; 2005 case 'n_fileas': 2006 if (!$row['n_fileas']) $row['n_fileas'] = $this->fileas($row); 2007 list($row['line1'],$row['line2']) = explode(': ',$row['n_fileas']); 2008 break; 2009 } 2010 if (isset($this->grouped_views[(string) $query['grouped_view']])) 2011 { 2012 $row['type'] = 'home'; 2013 $row['type_label'] = $query['grouped_view'] == 'duplicate' ? lang('Duplicates') : lang('Organisation'); 2014 2015 if ($query['filter'] && !($this->grants[(int)$query['filter']] & Acl::DELETE)) 2016 { 2017 $row['class'] .= 'rowNoDelete '; 2018 } 2019 $row['class'] .= 'rowNoEdit '; // no edit in OrgView 2020 $row['class'] .= $query['grouped_view'] == 'duplicates' ? 'contact_duplicate' : 'contact_organisation '; 2021 } 2022 else 2023 { 2024 $this->type_icon($row['owner'],$row['private'],$row['tid'],$row['type'],$row['type_label']); 2025 2026 static $tel2show = array('tel_work','tel_cell','tel_home','tel_fax'); 2027 static $prefer_marker = null; 2028 if (is_null($prefer_marker)) 2029 { 2030 // as et2 adds options with .text(), it can't be entities, but php knows no string literals with utf-8 2031 $prefer_marker = html_entity_decode(' ☆', ENT_NOQUOTES, 'utf-8'); 2032 } 2033 foreach($tel2show as $name) 2034 { 2035 $row[$name] .= ' '.($row['tel_prefer'] == $name ? $prefer_marker : ''); // .' ' to NOT remove the field 2036 } 2037 // allways show the prefered phone, if not already shown 2038 if (!in_array($row['tel_prefer'],$tel2show) && $row[$row['tel_prefer']]) 2039 { 2040 $row['tel_prefered'] = $row[$row['tel_prefer']].$prefer_marker; 2041 } 2042 // Show nice name as status text 2043 if($row['tel_prefer']) 2044 { 2045 $row['tel_prefer_label'] = $this->contact_fields[$row['tel_prefer']]; 2046 } 2047 if (!$row['owner'] && $row['account_id'] > 0) 2048 { 2049 $row['class'] .= 'rowAccount rowNoDelete '; 2050 } 2051 elseif (!$this->check_perms(Acl::DELETE,$row) || (!$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge' && $query['col_filter']['tid'] == self::DELETED_TYPE)) 2052 { 2053 $row['class'] .= 'rowNoDelete '; 2054 } 2055 if (!$this->check_perms(Acl::EDIT,$row)) 2056 { 2057 $row['class'] .= 'rowNoEdit '; 2058 } 2059 $row['class'] .= 'contact_contact '; 2060 2061 unset($row['jpegphoto']); // unused and messes up json encoding (not utf-8) 2062 2063 if (isset($customfields[$row['id']])) 2064 { 2065 foreach($this->customfields as $name => $data) 2066 { 2067 $row['#'.$name] = $customfields[$row['id']]['#'.$name]; 2068 } 2069 } 2070 if (isset($distributionlist[$row['id']])) 2071 { 2072 $row['distrib_lists'] = implode("\n",array_values($distributionlist[$row['id']])); 2073 //if ($show_distributionlist) $readonlys['distrib_lists'] =true; 2074 } 2075 if (isset($calendar[$calendar_participants[$row['id']]])) 2076 { 2077 foreach($calendar[$calendar_participants[$row['id']]] as $name => $data) 2078 { 2079 $row[$name] = $data; 2080 } 2081 } 2082 } 2083 2084 // hide region for address format 'postcode_city' 2085 if (($row['addr_format'] = $this->addr_format_by_country($row['adr_one_countryname']))=='postcode_city') unset($row['adr_one_region']); 2086 if (($row['addr_format2'] = $this->addr_format_by_country($row['adr_two_countryname']))=='postcode_city') unset($row['adr_two_region']); 2087 2088 // respect category permissions 2089 if(!empty($row['cat_id'])) 2090 { 2091 $row['cat_id'] = $this->categories->check_list(Acl::READ,$row['cat_id']); 2092 } 2093 2094 if ($query['col_filter']['shared_by'] == $this->user || !empty($row['shared_with']) && 2095 array_intersect($unshare_grants, explode(',', $row['shared_with']))) 2096 { 2097 $row['class'] .= 'unshare_contact '; 2098 } 2099 } 2100 $rows['no_distribution_list'] = (bool)$query['filter2']; 2101 2102 // disable customfields column, if we have no customefield(s) 2103 if (!$this->customfields) 2104 { 2105 $rows['no_customfields'] = true; 2106 } 2107 2108 // Disable next/last date if so configured 2109 if($this->config['disable_event_column'] == 'True') 2110 { 2111 $rows['no_event_column'] = true; 2112 } 2113 2114 // If we've changed the sort order on them, update the display 2115 if($order !== $query['order'] ) 2116 { 2117 $rows['order'] = $order; 2118 } 2119 $rows['call_popup'] = $this->config['call_popup']; 2120 $rows['customfields'] = array_values($this->customfields); 2121 2122 // full app-header with all search criteria specially for the print 2123 $header = array(); 2124 if ($query['filter'] !== '' && !isset($this->grouped_views[$query['grouped_view']])) 2125 { 2126 $header[] = ($query['filter'] == '0' ? lang('accounts') : 2127 ($GLOBALS['egw']->accounts->get_type($query['filter']) == 'g' ? 2128 lang('Group %1',$GLOBALS['egw']->accounts->id2name($query['filter'])) : 2129 Api\Accounts::username((int)$query['filter']). 2130 (substr($query['filter'],-1) == 'p' ? ' ('.lang('private').')' : ''))); 2131 } 2132 if ($query['grouped_view']) 2133 { 2134 $header[] = $query['grouped_view_label']; 2135 // Make sure option is there 2136 if(!array_key_exists($query['grouped_view'], $this->grouped_views)) 2137 { 2138 $this->grouped_views += $this->_get_grouped_name($query['grouped_view']); 2139 $rows['sel_options']['grouped_view'] = $this->grouped_views; 2140 } 2141 } 2142 if($query['advanced_search']) 2143 { 2144 $header[] = lang('Advanced search'); 2145 } 2146 if ($query['cat_id']) 2147 { 2148 $header[] = lang('Category').' '.$GLOBALS['egw']->categories->id2name($query['cat_id']); 2149 } 2150 if ($query['searchletter']) 2151 { 2152 $order = $order == 'n_given' ? lang('first name') : ($order == 'n_family' ? lang('last name') : lang('Organisation')); 2153 $header[] = lang("%1 starts with '%2'",$order,$query['searchletter']); 2154 } 2155 if ($query['search'] && !$query['advanced_search']) // do not add that, if we have advanced search active 2156 { 2157 $header[] = lang("Search for '%1'",$query['search']); 2158 } 2159 $GLOBALS['egw_info']['flags']['app_header'] = implode(': ', $header); 2160 2161 if ($query['grouped_view'] === '' && $query['col_filter']['shared_by'] == $this->user) 2162 { 2163 $query['grouped_view'] = 'shared_by_me'; 2164 unset($query['col_filter']['shared_by']); 2165 } 2166 return $this->total; 2167 } 2168 2169 /** 2170 * Get addressbook type icon from owner, private and tid 2171 * 2172 * @param int $owner user- or group-id or 0 for Api\Accounts 2173 * @param boolean $private 2174 * @param string $tid 'n' for regular addressbook 2175 * @param string &$icon icon-name 2176 * @param string &$label translated label 2177 */ 2178 function type_icon($owner,$private,$tid,&$icon,&$label) 2179 { 2180 if (!$owner) 2181 { 2182 $icon = 'accounts'; 2183 $label = lang('accounts'); 2184 } 2185 elseif ($private) 2186 { 2187 $icon = 'private'; 2188 $label = lang('private'); 2189 } 2190 elseif ($GLOBALS['egw']->accounts->get_type($owner) == 'g') 2191 { 2192 $icon = 'group'; 2193 $label = lang('group %1',$GLOBALS['egw']->accounts->id2name($owner)); 2194 } 2195 else 2196 { 2197 $icon = 'personal'; 2198 $label = $owner == $this->user ? lang('personal') : Api\Accounts::username($owner); 2199 } 2200 // show tid icon for tid!='n' AND only if one is defined 2201 if ($tid != 'n' && Api\Image::find('addressbook',$this->content_types[$tid]['name'])) 2202 { 2203 $icon = Api\Image::find('addressbook',$this->content_types[$tid]['name']); 2204 } 2205 2206 // Legacy - from when icons could be anywhere 2207 if ($tid != 'n' && $this->content_types[$tid]['options']['icon']) 2208 { 2209 $icon = $this->content_types[$tid]['options']['icon']; 2210 $label = $this->content_types[$tid]['name'].' ('.$label.')'; 2211 } 2212 } 2213 2214 /** 2215 * Edit a contact 2216 * 2217 * @param array $content=null submitted content 2218 * @param int $_GET['contact_id'] contact_id mainly for popup use 2219 * @param bool $_GET['makecp'] true if you want to copy the contact given by $_GET['contact_id'] 2220 */ 2221 function edit($content=null) 2222 { 2223 if (is_array($content)) 2224 { 2225 // sync $content['shared'] with $content['shared_values'] 2226 foreach($content['shared'] as $key => $shared) 2227 { 2228 $shared_value = $shared['shared_id'].':'.$shared['shared_with'].':'.$shared['shared_by'].':'.$shared['shared_writable']; 2229 if (($k = array_search($shared_value, (array)$content['shared_values'])) === false) 2230 { 2231 unset($content['shared'][$key]); 2232 } 2233 else 2234 { 2235 unset($content['shared_values'][$k]); 2236 } 2237 } 2238 foreach((array)$content['shared_values'] as $account_id) 2239 { 2240 $content['shared'][] = [ 2241 'contact_id' => $content['id'], 2242 'contact' => $content, 2243 'shared_with' => $account_id, 2244 'shared_by' => $this->user, 2245 'shared_at' => new Api\DateTime(), 2246 'shared_writable' => (int)(bool)$content['shared_writable'], 2247 ]; 2248 } 2249 unset($content['shared_values']); 2250 // remove invalid shared-with entries (should not happen, as we validate already on client-side) 2251 $this->check_shared_with($content['shared']); 2252 2253 $button = @key($content['button']); 2254 unset($content['button']); 2255 $content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p'); 2256 $content['owner'] = (string) (int) $content['owner']; 2257 $content['cat_id'] = $this->config['cat_tab'] === 'Tree' ? $content['cat_id_tree'] : $content['cat_id']; 2258 if ($this->config['private_cf_tab']) $content = (array)$content['private_cfs'] + $content; 2259 unset($content['private_cfs']); 2260 2261 switch($button) 2262 { 2263 case 'save': 2264 case 'apply': 2265 if ($content['presets_fields']) 2266 { 2267 // unset the duplicate_filed after submit because we don't need to warn user for second time about contact duplication 2268 unset($content['presets_fields']); 2269 } 2270 // photo might be changed by ajax_upload_photo 2271 if (!array_key_exists('jpegphoto', $content)) 2272 { 2273 $content['photo_unchanged'] = true; // hint no need to store photo 2274 } 2275 $links = false; 2276 if (!$content['id'] && is_array($content['link_to']['to_id'])) 2277 { 2278 $links = $content['link_to']['to_id']; 2279 } 2280 $fullname = $old_fullname = parent::fullname($content); 2281 if ($content['id'] && $content['org_name'] && $content['change_org']) 2282 { 2283 $old_org_entry = $this->read($content['id']); 2284 $old_fullname = ($old_org_entry['n_fn'] ? $old_org_entry['n_fn'] : parent::fullname($old_org_entry)); 2285 } 2286 if ( $content['n_fn'] != $fullname || $fullname != $old_fullname) 2287 { 2288 unset($content['n_fn']); 2289 } 2290 // Country codes 2291 foreach(array('adr_one', 'adr_two') as $c_prefix) 2292 { 2293 if ($content[$c_prefix.'_countrycode'] == '-custom-') 2294 { 2295 $content[$c_prefix.'_countrycode'] = null; 2296 } 2297 } 2298 $content['msg'] = ''; 2299 $this->error = false; 2300 foreach((array)$content['pre_save_callbacks'] as $callback) 2301 { 2302 try { 2303 if (($success_msg = call_user_func_array($callback, array(&$content)))) 2304 { 2305 $content['msg'] .= ($content['msg'] ? ', ' : '').$success_msg; 2306 } 2307 } 2308 catch (Exception $ex) { 2309 $content['msg'] .= ($content['msg'] ? ', ' : '').$ex->getMessage(); 2310 $button = 'apply'; // do not close dialog 2311 $this->error = true; 2312 break; 2313 } 2314 } 2315 if ($this->error) 2316 { 2317 // error in pre_save_callbacks 2318 } 2319 elseif ($this->save($content)) 2320 { 2321 $content['msg'] .= ($content['msg'] ? ', ' : '').lang('Contact saved'); 2322 2323 unset($content['jpegphoto'], $content['photo_unchanged']); 2324 2325 foreach((array)$content['post_save_callbacks'] as $callback) 2326 { 2327 try { 2328 if (($success_msg = call_user_func_array($callback, array(&$content)))) 2329 { 2330 $content['msg'] .= ', '.$success_msg; 2331 } 2332 } 2333 catch(Api\Exception\Redirect $r) 2334 { 2335 // catch it to continue execution and rethrow it later 2336 } 2337 catch (Exception $ex) { 2338 $content['msg'] .= ', '.$ex->getMessage(); 2339 $button = 'apply'; // do not close dialog 2340 $this->error = true; 2341 break; 2342 } 2343 } 2344 2345 if ($content['change_org'] && $old_org_entry && ($changed = $this->changed_fields($old_org_entry,$content,true)) && 2346 ($members = $this->org_similar($old_org_entry['org_name'],$changed))) 2347 { 2348 //foreach($changed as $name => $old_value) echo "<p>$name: '$old_value' --> '{$content[$name]}'</p>\n"; 2349 list($changed_members,$changed_fields,$failed_members) = $this->change_org($old_org_entry['org_name'],$changed,$content,$members); 2350 if ($changed_members) 2351 { 2352 $content['msg'] .= ', '.lang('%1 fields in %2 other organisation member(s) changed',$changed_fields,$changed_members); 2353 } 2354 if ($failed_members) 2355 { 2356 $content['msg'] .= ', '.lang('failed to change %1 organisation member(s) (insufficent rights) !!!',$failed_members); 2357 } 2358 } 2359 } 2360 elseif($this->error === true) 2361 { 2362 $content['msg'] = lang('Error: the entry has been updated since you opened it for editing!').'<br />'. 2363 lang('Copy your changes to the clipboard, %1reload the entry%2 and merge them.','<a href="'. 2364 htmlspecialchars(Egw::link('/index.php',array( 2365 'menuaction' => 'addressbook.addressbook_ui.edit', 2366 'contact_id' => $content['id'], 2367 ))).'">','</a>'); 2368 break; // dont refresh the list 2369 } 2370 else 2371 { 2372 $content['msg'] = lang('Error saving the contact !!!'). 2373 ($this->error ? ' '.$this->error : ''); 2374 $button = 'apply'; // to not leave the dialog 2375 } 2376 // writing links for new entry, existing ones are handled by the widget itself 2377 if ($links && $content['id']) 2378 { 2379 Link::link('addressbook',$content['id'],$links); 2380 } 2381 // Update client side global datastore 2382 $response = Api\Json\Response::get(); 2383 $response->generic('data', array('uid' => 'addressbook::'.$content['id'], 'data' => $content)); 2384 Framework::refresh_opener($content['msg'], 'addressbook', $content['id'], $content['id'] ? 'edit' : 'add', 2385 null, null, null, $this->error ? 'error' : 'success'); 2386 2387 // re-throw redirect exception, if there's no error 2388 if (!$this->error && isset($r)) 2389 { 2390 throw $r; 2391 } 2392 if ($button == 'save') 2393 { 2394 Framework::window_close(); 2395 } 2396 else 2397 { 2398 Framework::message($content['msg'], $this->error ? 'error' : 'success'); 2399 unset($content['msg']); 2400 } 2401 $content['link_to']['to_id'] = $content['id']; 2402 break; 2403 2404 case 'delete': 2405 $success = $failed = $action_msg = null; 2406 if($this->action('delete',array($content['id']),false,$success,$failed,$action_msg,'',$content['msg'])) 2407 { 2408 if ($GLOBALS['egw']->currentapp == 'addressbook') 2409 { 2410 Framework::refresh_opener(lang('Contact deleted'), 'addressbook', $content['id'], 'delete' ); 2411 Framework::window_close(); 2412 } 2413 else 2414 { 2415 Framework::refresh_opener(lang('Contact deleted'), 'addressbook', $content['id'], null, 'addressbook'); 2416 Framework::window_close(); 2417 } 2418 } 2419 else 2420 { 2421 $content['msg'] = lang('Error deleting the contact !!!'); 2422 } 2423 break; 2424 } 2425 $view = !$this->check_perms(Acl::EDIT, $content); 2426 } 2427 else 2428 { 2429 $content = array(); 2430 $contact_id = $_GET['contact_id'] ? $_GET['contact_id'] : ((int)$_GET['account_id'] ? 'account:'.(int)$_GET['account_id'] : 0); 2431 $view = (boolean)$_GET['view']; 2432 // new contact --> set some defaults 2433 if ($contact_id && is_array($content = $this->read($contact_id))) 2434 { 2435 $contact_id = $content['id']; // it could have been: "account:$account_id" 2436 if (!$this->check_perms(Acl::EDIT, $content)) 2437 { 2438 $view = true; 2439 } 2440 } 2441 else // not found 2442 { 2443 $state = Api\Cache::getSession('addressbook', 'index'); 2444 // check if we create the new contact in an existing org 2445 if (($org = $_GET['org'])) 2446 { 2447 // arguments containing a comma get quoted by etemplate/js/nextmatch_action.js 2448 // leading to error in Api\Db::column_data_implode, if not unquoted 2449 if ($org[0] == '"') $org = substr($org, 1, -1); 2450 $content = $this->read_org($org); 2451 } 2452 elseif ($state['grouped_view'] && !isset($this->grouped_views[$state['grouped_view']])) 2453 { 2454 $content = $this->read_org($state['grouped_view']); 2455 } 2456 else 2457 { 2458 if ($GLOBALS['egw_info']['user']['preferences']['common']['country']) 2459 { 2460 $content['adr_one_countrycode'] = 2461 $GLOBALS['egw_info']['user']['preferences']['common']['country']; 2462 $content['adr_one_countryname'] = 2463 $GLOBALS['egw']->country->get_full_name($GLOBALS['egw_info']['user']['preferences']['common']['country']); 2464 $content['adr_two_countrycode'] = 2465 $GLOBALS['egw_info']['user']['preferences']['common']['country']; 2466 $content['adr_two_countryname'] = 2467 $GLOBALS['egw']->country->get_full_name($GLOBALS['egw_info']['user']['preferences']['common']['country']); 2468 } 2469 if ($this->prefs['fileas_default']) $content['fileas_type'] = $this->prefs['fileas_default']; 2470 } 2471 if (isset($_GET['owner']) && $_GET['owner'] !== '') 2472 { 2473 $content['owner'] = $_GET['owner']; 2474 } 2475 else 2476 { 2477 $content['owner'] = (string)($state['filter'] == 0 ? '' : $state['filter']); 2478 } 2479 $content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p'); 2480 if ($content['owner'] === '' || !($this->grants[$content['owner'] = (string) (int) $content['owner']] & Acl::ADD)) 2481 { 2482 $content['owner'] = $this->default_addressbook; 2483 $content['private'] = (int)$this->default_private; 2484 2485 if (!($this->grants[$content['owner'] = (string) (int) $content['owner']] & Acl::ADD)) 2486 { 2487 $content['owner'] = (string) $this->user; 2488 $content['private'] = 0; 2489 } 2490 } 2491 $new_type = array_keys($this->content_types); 2492 // fetch active type to preset the type, if param typeid is not passed 2493 $active_tid = Api\Cache::getSession('addressbook','active_tid'); 2494 if ($active_tid && strtoupper($active_tid) === 'D') unset($active_tid); 2495 $content['tid'] = $_GET['typeid'] ? $_GET['typeid'] : ($active_tid?$active_tid:$new_type[0]); 2496 foreach($this->get_contact_columns() as $field) 2497 { 2498 if ($_GET['presets'][$field]) 2499 { 2500 if ($field=='email'||$field=='email_home') 2501 { 2502 $singleAddress = imap_rfc822_parse_adrlist($_GET['presets'][$field],''); 2503 //error_log(__METHOD__.__LINE__.' Address:'.$singleAddress[0]->mailbox."@".$singleAddress[0]->host.", ".$singleAddress[0]->personal); 2504 if (!(!is_array($singleAddress) || count($singleAddress)<1)) 2505 { 2506 $content[$field] = $singleAddress[0]->mailbox."@".$singleAddress[0]->host; 2507 if (!empty($singleAddress[0]->personal)) 2508 { 2509 if (strpos($singleAddress[0]->personal,',')===false) 2510 { 2511 list($P_n_given,$P_n_family,$P_org_name)=explode(' ',$singleAddress[0]->personal,3); 2512 if (strlen(trim($P_n_given))>0) $content['n_given'] = trim($P_n_given); 2513 if (strlen(trim($P_n_family))>0) $content['n_family'] = trim($P_n_family); 2514 if (strlen(trim($P_org_name))>0) $content['org_name'] = trim($P_org_name); 2515 } 2516 else 2517 { 2518 list($P_n_family,$P_other)=explode(',',$singleAddress[0]->personal,2); 2519 if (strlen(trim($P_n_family))>0) $content['n_family'] = trim($P_n_family); 2520 if (strlen(trim($P_other))>0) 2521 { 2522 list($P_n_given,$P_org_name)=explode(',',$P_other,2); 2523 if (strlen(trim($P_n_given))>0) $content['n_given'] = trim($P_n_given); 2524 if (strlen(trim($P_org_name))>0) $content['org_name'] = trim($P_org_name); 2525 } 2526 } 2527 } 2528 } 2529 else 2530 { 2531 $content[$field] = $_GET['presets'][$field]; 2532 } 2533 } 2534 else 2535 { 2536 $content[$field] = $_GET['presets'][$field]; 2537 } 2538 } 2539 } 2540 if (isset($_GET['presets'])) 2541 { 2542 foreach(array('email','email_home','n_family','n_given','org_name') as $field) 2543 { 2544 if (!empty($content[$field])) 2545 { 2546 //Set the presets fields in content in order to be able to use them later in client side for checking duplication only on first time load 2547 // after save/apply we unset them 2548 $content['presets_fields'][]= $field; 2549 break; 2550 } 2551 } 2552 if (empty($content['n_fn'])) $content['n_fn'] = $this->fullname($content); 2553 } 2554 $content['creator'] = $this->user; 2555 $content['created'] = $this->now_su; 2556 unset($state); 2557 //_debug_array($content); 2558 } 2559 2560 if ($_GET['msg']) $content['msg'] = strip_tags($_GET['msg']); // dont allow HTML! 2561 2562 if($content && $_GET['makecp']) // copy the contact 2563 { 2564 $this->copy_contact($content); 2565 $content['msg'] = lang('%1 copied - the copy can now be edited', lang(Link::get_registry('addressbook','entry'))); 2566 $view = false; 2567 } 2568 else 2569 { 2570 if ($contact_id && is_numeric($contact_id)) $content['link_to']['to_id'] = $contact_id; 2571 } 2572 // automatic link new entries to entries specified in the url 2573 if (!$contact_id && isset($_REQUEST['link_app']) && isset($_REQUEST['link_id']) && !is_array($content['link_to']['to_id'])) 2574 { 2575 $link_ids = is_array($_REQUEST['link_id']) ? $_REQUEST['link_id'] : array($_REQUEST['link_id']); 2576 foreach(is_array($_REQUEST['link_app']) ? $_REQUEST['link_app'] : array($_REQUEST['link_app']) as $n => $link_app) 2577 { 2578 $link_id = $link_ids[$n]; 2579 if (preg_match('/^[a-z_0-9-]+:[:a-z_0-9-]+$/i',$link_app.':'.$link_id)) // gard against XSS 2580 { 2581 Link::link('addressbook',$content['link_to']['to_id'],$link_app,$link_id); 2582 } 2583 } 2584 } 2585 } 2586 // set $content[shared_options/_values] from $content[shared] 2587 $content['shared_options'] = []; 2588 foreach((array)$content['shared'] as $shared) 2589 { 2590 $content['shared_options'][$shared['shared_id'].':'.$shared['shared_with'].':'.$shared['shared_by'].':'.$shared['shared_writable']] = [ 2591 'label' => Api\Accounts::username($shared['shared_with']), 2592 'title' => lang('%1 shared this contact on %2 with %3 %4', 2593 Api\Accounts::username($shared['shared_by']), Api\DateTime::to($shared['shared_at']), 2594 Api\Accounts::username($shared['shared_with']), $shared['shared_writable'] ? lang('writable') : lang('readonly')), 2595 'icon' => $shared['shared_writable'] ? 'edit' : 'view', 2596 ]; 2597 } 2598 $content['shared_values'] = array_keys($content['shared_options']); 2599 // disable shared with UI for non-SQL backends 2600 $content['shared_disabled'] = !is_a($this->get_backend($content['id'], $content['owner']), Api\Contacts\Sql::class); 2601 2602 if ($content['id']) 2603 { 2604 // last and next calendar date 2605 $dates = current($this->read_calendar(array($content['account_id'] ? $content['account_id'] : 'c'.$content['id']),false)); 2606 if(is_array($dates)) $content += $dates; 2607 } 2608 2609 // Registry has view_id as contact_id, so set it (custom fields uses it) 2610 $content['contact_id'] = $content['id']; 2611 2612 // Avoid ID conflict with tree & selectboxes 2613 $content['cat_id_tree'] = $content['cat_id']; 2614 2615 // Avoid setting conflicts with private custom fields 2616 $content['private_cfs'] = array(); 2617 foreach(Api\Storage\Customfields::get('addressbook', true) as $name => $cf) 2618 { 2619 if ($this->config['private_cf_tab'] && $cf['private'] && isset($content['#'.$name])) 2620 { 2621 $content['private_cfs']['#'.$name] = $content['#'.$name]; 2622 } 2623 } 2624 2625 // how to display addresses 2626 $content['addr_format'] = $this->addr_format_by_country($content['adr_one_countryname']); 2627 $content['addr_format2'] = $this->addr_format_by_country($content['adr_two_countryname']); 2628 2629 //_debug_array($content); 2630 $readonlys['button[delete]'] = !$content['owner'] || !$this->check_perms(Acl::DELETE,$content); 2631 $readonlys['button[copy]'] = $readonlys['button[edit]'] = $readonlys['button[vcard]'] = true; 2632 $readonlys['button[save]'] = $readonlys['button[apply]'] = $view; 2633 if ($view) 2634 { 2635 $readonlys['__ALL__'] = true; 2636 $readonlys['button[cancel]'] = false; 2637 } 2638 2639 $sel_options['fileas_type'] = $this->fileas_options($content); 2640 $sel_options['adr_one_countrycode']['-custom-'] = lang('Custom'); 2641 $sel_options['owner'] = $this->get_addressbooks(Acl::ADD); 2642 if ($content['owner']) unset($sel_options['owner'][0]); // do not offer to switch to accounts, as we do not support moving contacts to accounts 2643 if ((string) $content['owner'] !== '') 2644 { 2645 if (!isset($sel_options['owner'][(int)$content['owner']])) 2646 { 2647 $sel_options['owner'][(int)$content['owner']] = !$content['owner'] ? lang('Accounts') : 2648 Api\Accounts::username($content['owner']); 2649 } 2650 $readonlys['owner'] = !$content['owner'] || // dont allow to move accounts, as this mean deleting the user incl. all content he owns 2651 $content['id'] && !$this->check_perms(Acl::DELETE,$content); // you need delete rights to move an existing contact into an other addressbook 2652 } 2653 // set the unsupported fields from the backend to readonly 2654 foreach($this->get_fields('unsupported',$content['id'],$content['owner']) as $field) 2655 { 2656 $readonlys[$field] = true; 2657 } 2658 // for editing own account, make all fields not allowed by own_account_acl readonly 2659 if (!$this->is_admin() && !$content['owner'] && $content['account_id'] == $this->user && $this->own_account_acl && !$view) 2660 { 2661 $readonlys['__ALL__'] = true; 2662 $readonlys['button[cancel]'] = false; 2663 2664 foreach($this->own_account_acl as $field) 2665 { 2666 $readonlys[$field] = false; 2667 } 2668 if (!$readonlys['jpegphoto']) 2669 { 2670 $readonlys = array_merge($readonlys, array( 2671 'upload_photo' => false, 2672 'delete_photo' => false, 2673 'addressbook.edit.upload' => false 2674 )); 2675 } 2676 if (!$readonlys['pubkey']) 2677 { 2678 $readonlys['addressbook:'.$content['id'].':.files/pgp-pubkey.asc'] = 2679 $readonlys['addressbook:'.$content['id'].':.files/smime-pubkey.crt'] = false; 2680 } 2681 } 2682 2683 if (isset($readonlys['n_fileas'])) $readonlys['fileas_type'] = $readonlys['n_fileas']; 2684 // disable not needed tabs 2685 $readonlys['tabs']['cats'] = !($content['cat_tab'] = $this->config['cat_tab']); 2686 $readonlys['tabs']['custom'] = !$this->customfields || $this->get_backend($content['id'],$content['owner']) == $this->so_accounts; 2687 $readonlys['tabs']['custom_private'] = $readonlys['tabs']['custom'] || !$this->config['private_cf_tab']; 2688 $readonlys['tabs']['distribution_list'] = !$content['distrib_lists'];#false; 2689 $readonlys['tabs']['history'] = $this->contact_repository != 'sql' || !$content['id'] || 2690 $this->account_repository != 'sql' && $content['account_id']; 2691 if (!$content['id']) $readonlys['button[delete]'] = !$content['id']; 2692 if ($this->config['private_cf_tab']) $content['no_private_cfs'] = 0; 2693 $readonlys['change_org'] = empty($content['org_name']) || $view; 2694 2695 // for editing the own account (by a non-admin), enable only the fields allowed via the "own_account_acl" 2696 if (!$content['owner'] && !$this->check_perms(Acl::EDIT, $content)) 2697 { 2698 $this->_set_readonlys_for_own_account_acl($readonlys, $content['id']); 2699 } 2700 for($i = -23; $i<=23; $i++) 2701 { 2702 $tz[$i] = ($i > 0 ? '+' : '').$i; 2703 } 2704 $sel_options['tz'] = $tz; 2705 $content['tz'] = $content['tz'] ? $content['tz'] : '0'; 2706 if (count($this->content_types) > 1) 2707 { 2708 foreach($this->content_types as $type => $data) 2709 { 2710 $sel_options['tid'][$type] = $data['name']; 2711 } 2712 $content['typegfx'] = Api\Html::image('addressbook',$this->content_types[$content['tid']]['options']['icon'],'',' width="16px" height="16px"'); 2713 } 2714 else 2715 { 2716 $content['no_tid'] = true; 2717 } 2718 2719 $content['view'] = false; 2720 $content['link_to'] = array( 2721 'to_app' => 'addressbook', 2722 'to_id' => $content['link_to']['to_id'], 2723 ); 2724 2725 // Links for deleted entries 2726 if($content['tid'] == self::DELETED_TYPE) 2727 { 2728 $content['link_to']['show_deleted'] = true; 2729 if(!$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge') 2730 { 2731 $readonlys['button[delete]'] = true; 2732 } 2733 } 2734 2735 // Enable history 2736 $this->setup_history($content, $sel_options); 2737 2738 $content['photo'] = $this->photo_src($content['id'],$content['jpegphoto'],'',$content['etag']); 2739 2740 if ($content['private']) $content['owner'] .= 'p'; 2741 2742 // for custom types, check if we have a custom edit template named "addressbook.edit.$type", $type is the name 2743 if (in_array($content['tid'], array('n',self::DELETED_TYPE)) || !$this->tmpl->read('addressbook.edit.'.$this->content_types[$content['tid']]['name'])) 2744 { 2745 $this->tmpl->read('addressbook.edit'); 2746 } 2747 2748 // allow other apps to add tabs to addressbook edit 2749 $preserve = $content; 2750 $preserve['old_owner'] = $content['owner']; 2751 unset($preserve['jpegphoto'], $content['jpegphoto']); // unused and messes up json encoding (not utf-8) 2752 $this->tmpl->setElementAttribute('tabs', 'add_tabs', true); 2753 $tabs =& $this->tmpl->getElementAttribute('tabs', 'tabs'); 2754 if (($first_call = !isset($tabs))) 2755 { 2756 $tabs = array(); 2757 } 2758 //error_log(__LINE__.': '.__METHOD__."() first_call=$first_call"); 2759 $hook_data = Api\Hooks::process(array('location' => 'addressbook_edit')+$content); 2760 //error_log(__METHOD__."() hook_data=".array2string($hook_data)); 2761 foreach($hook_data as $extra_tabs) 2762 { 2763 if (!$extra_tabs) continue; 2764 2765 foreach(isset($extra_tabs[0]) ? $extra_tabs : array($extra_tabs) as $extra_tab) 2766 { 2767 if ($extra_tab['data'] && is_array($extra_tab['data'])) 2768 { 2769 $content = array_merge($content, $extra_tab['data']); 2770 } 2771 if ($extra_tab['preserve'] && is_array($extra_tab['preserve'])) 2772 { 2773 $preserve = array_merge($preserve, $extra_tab['preserve']); 2774 } 2775 if ($extra_tab['readonlys'] && is_array($extra_tab['readonlys'])) 2776 { 2777 $readonlys = array_merge($readonlys, $extra_tab['readonlys']); 2778 } 2779 // we must NOT add tabs and callbacks more then once! 2780 if (!$first_call) continue; 2781 2782 if (!empty($extra_tab['pre_save_callback'])) 2783 { 2784 $preserve['pre_save_callbacks'][] = $extra_tab['pre_save_callback']; 2785 } 2786 if (!empty($extra_tab['post_save_callback'])) 2787 { 2788 $preserve['post_save_callbacks'][] = $extra_tab['post_save_callback']; 2789 } 2790 if (!empty($extra_tab['label']) && !empty($extra_tab['name'])) 2791 { 2792 $tabs[] = array( 2793 'label' => $extra_tab['label'], 2794 'template' => $extra_tab['name'], 2795 'prepend' => $extra_tab['prepend'], 2796 ); 2797 } 2798 //error_log(__METHOD__."() changed tabs=".array2string($tabs)); 2799 } 2800 } 2801 return $this->tmpl->exec('addressbook.addressbook_ui.edit', $content, $sel_options, $readonlys, $preserve, 2); 2802 } 2803 2804 /** 2805 * Check if user has right to share with / into given AB 2806 * 2807 * @param array $_data values for keys "shared_writable", "shared_values" and "contact" 2808 * @return array of entries removed from $shared_with because current user is not allowed to share into 2809 */ 2810 public function ajax_check_shared(array $_data) 2811 { 2812 $response = Api\Json\Response::get(); 2813 try { 2814 $shared = []; 2815 foreach($_data['shared_values'] as $value) 2816 { 2817 if (is_numeric($value)) 2818 { 2819 $shared[$value] = [ 2820 'shared_with' => $value, 2821 'shared_by' => $this->user, 2822 'shared_writable' => (int)$_data['shared_writable'], 2823 ]; 2824 } 2825 else 2826 { 2827 $shared[$value] = array_combine(['shared_id', 'shared_with', 'shared_by', 'shared_writable'], explode(':', $value)); 2828 } 2829 $shared[$value]['contact'] = $_data['contact']; 2830 } 2831 if (($failed = $this->check_shared_with($shared, $error))) 2832 { 2833 $response->data(array_keys($failed)); 2834 $response->message($error ?: lang('You are not allowed to share into the addressbook of %1', 2835 implode(', ', array_map(function ($data) { 2836 return Api\Accounts::username($data['shared_with']); 2837 }, $failed))), 'error'); 2838 } 2839 } 2840 catch (\Exception $e) { 2841 $response->message($e->getMessage(), 'error'); 2842 } 2843 } 2844 2845 /** 2846 * Set the readonlys for non-admins editing their own account 2847 * 2848 * @param array &$readonlys 2849 * @param int $id 2850 */ 2851 function _set_readonlys_for_own_account_acl(&$readonlys,$id) 2852 { 2853 // regular fields depending on the backend 2854 foreach($this->get_fields('supported',$id,0) as $field) 2855 { 2856 if (!$this->own_account_acl || !in_array($field,$this->own_account_acl)) 2857 { 2858 $readonlys[$field] = true; 2859 switch($field) 2860 { 2861 case 'tel_work': 2862 case 'tel_cell': 2863 case 'tel_home': 2864 $readonlys[$field.'2'] = true; 2865 break; 2866 case 'n_fileas': 2867 $readonlys['fileas_type'] = true; 2868 break; 2869 } 2870 } 2871 } 2872 // custom fields 2873 if ($this->customfields) 2874 { 2875 foreach(array_keys($this->customfields) as $name) 2876 { 2877 if (!$this->own_account_acl || !in_array('#'.$name,$this->own_account_acl)) 2878 { 2879 $readonlys['#'.$name] = true; 2880 } 2881 } 2882 } 2883 // links 2884 if (!$this->own_account_acl || !in_array('link_to',$this->own_account_acl)) 2885 { 2886 $readonlys['link_to'] = true; 2887 } 2888 } 2889 2890 /** 2891 * Doublicate check: returns similar contacts: same email or 2 of name, firstname, org 2892 * 2893 * Also update/return fileas options, if necessary. 2894 * 2895 * @param array $values contact values from form 2896 * @param string $name name of changed value, eg. "email" 2897 * @param int $own_id =0 own contact id, to not check against it 2898 * @return array with keys 'msg' => "EMail address exists, do you want to open contact?" (or null if not existing) 2899 * 'data' => array of id => "full name (addressbook)" pairs 2900 * 'fileas_options' 2901 */ 2902 public function ajax_check_values($values, $name, $own_id=0) 2903 { 2904 $fields = explode(',',$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_fields']); 2905 $threshold = (int)$GLOBALS['egw_info']['user']['preferences']['addressbook']['duplicate_threshold']; 2906 2907 $ret = array('doublicates' => array(), 'msg' => null); 2908 2909 // if email changed, check for doublicates 2910 if (in_array($name, array('email', 'email_home')) && in_array('contact_'.$name, $fields)) 2911 { 2912 if (preg_match(Etemplate\Widget\Url::EMAIL_PREG, $values[$name])) // only search for real email addresses, to not return to many contacts 2913 { 2914 $contacts = parent::search(array( 2915 'email' => $values[$name], 2916 'email_home' => $values[$name], 2917 ), false, '', '', '', false, 'OR'); 2918 } 2919 } 2920 else 2921 { 2922 // only set fileas-options if other then email changed 2923 $ret['fileas_options'] = array_values($this->fileas_options($values)); 2924 // Full options for et2 2925 $ret['fileas_sel_options'] = $this->fileas_options($values); 2926 2927 // if name, firstname or org changed and enough are specified, check for doublicates 2928 $specified_count = 0; 2929 foreach($fields as $field) 2930 { 2931 if($values[trim($field)]) 2932 { 2933 $specified_count++; 2934 } 2935 } 2936 if (in_array($name,$fields) && $specified_count >= $threshold) 2937 { 2938 $filter = array(); 2939 foreach($fields as $n) // use email too, to exclude obvious false positives 2940 { 2941 if (!empty($values[$n])) $filter[$n] = $values[$n]; 2942 } 2943 $contacts = parent::search('', false, '', '', '', false, 'AND', false, $filter); 2944 } 2945 } 2946 if ($contacts) 2947 { 2948 foreach($contacts as $contact) 2949 { 2950 if ($own_id && $contact['id'] == $own_id) continue; 2951 2952 $ret['doublicates'][$contact['id']] = $this->fileas($contact).' ('. 2953 (!$contact['owner'] ? lang('Accounts') : ($contact['owner'] == $this->user ? 2954 ($contact['private'] ? lang('Private') : lang('Personal')) : Api\Accounts::username($contact['owner']))).')'; 2955 } 2956 if ($ret['doublicates']) 2957 { 2958 $ret['msg'] = lang('Similar contacts found:'). 2959 "\n\n".implode("\n", $ret['doublicates'])."\n\n". 2960 lang('Open for editing?'); 2961 } 2962 } 2963 //error_log(__METHOD__.'('.array2string($values).", '$name', $own_id) doublicates found ".array2string($ret['doublicates'])); 2964 Api\Json\Response::get()->data($ret); 2965 } 2966 2967 /** 2968 * CRM view 2969 * 2970 * @param array $content 2971 */ 2972 function view(array $content=null) 2973 { 2974 // CRM list comes from content, request, or preference 2975 $crm_list = $content['crm_list'] ? $content['crm_list'] : 2976 ($_GET['crm_list'] ? $_GET['crm_list'] : $GLOBALS['egw_info']['user']['preferences']['addressbook']['crm_list']); 2977 if(!$crm_list || $crm_list == '~edit~') $crm_list = 'infolog'; 2978 2979 if(is_array($content)) 2980 { 2981 $button = key($content['button']); 2982 switch ($button) 2983 { 2984 case 'vcard': 2985 Egw::redirect_link('/index.php','menuaction=addressbook.uivcard.out&ab_id=' .$content['id']); 2986 2987 case 'cancel': 2988 Egw::redirect_link('/index.php','menuaction=addressbook.addressbook_ui.index&ajax=true'); 2989 2990 case 'delete': 2991 Egw::redirect_link('/index.php',array( 2992 'menuaction' => 'addressbook.addressbook_ui.index', 2993 'msg' => $this->delete($content) ? lang('Contact deleted') : lang('Error deleting the contact !!!'), 2994 )); 2995 2996 case 'next': 2997 $inc = 1; 2998 // fall through 2999 case 'back': 3000 if (!isset($inc)) $inc = -1; 3001 // get next/previous contact in selection 3002 $query = Api\Cache::getSession('addressbook', 'index'); 3003 $query['start'] = $content['index'] + $inc; 3004 $query['num_rows'] = 1; 3005 $rows = $readonlys = array(); 3006 $num_rows = $this->get_rows($query, $rows, $readonlys, true); 3007 //error_log(__METHOD__."() get_rows()=$num_rows rows=".array2string($rows)); 3008 $contact_id = $rows[0]; 3009 if(!$contact_id || !is_array($content = $this->read($contact_id))) 3010 { 3011 Egw::redirect_link('/index.php',array( 3012 'menuaction' => 'addressbook.addressbook_ui.index', 3013 'msg' => $content, 3014 'ajax' => 'true' 3015 )); 3016 } 3017 $content['index'] = $query['start']; 3018 3019 // List nextmatch is already there, just update the filter 3020 if($contact_id && Api\Json\Request::isJSONRequest()) 3021 { 3022 switch($crm_list) 3023 { 3024 case 'infolog-organisation': 3025 $contact_id = $this->get_all_org_contacts($contact_id); 3026 // Fall through 3027 case 'infolog': 3028 case 'tracker': 3029 default: 3030 Api\Json\Response::get()->apply('app.addressbook.view_set_list',Array(Array('action'=>'addressbook', 'action_id' => $contact_id))); 3031 break; 3032 } 3033 3034 // Clear contact_id, it's used as a flag to send the list 3035 unset($contact_id); 3036 } 3037 break; 3038 } 3039 } 3040 else 3041 { 3042 // allow to search eg. for a phone number 3043 if (isset($_GET['search'])) 3044 { 3045 $query = Api\Cache::getSession('addressbook', 'index'); 3046 $query['search'] = $_GET['search']; 3047 unset($_GET['search']); 3048 // reset all filters 3049 unset($query['advanced_search']); 3050 $query['col_filter'] = array(); 3051 $query['filter'] = $query['filter2'] = $query['cat_id'] = ''; 3052 Api\Cache::setSession('addressbook', 'index', $query); 3053 $query['start'] = 0; 3054 $query['num_rows'] = 1; 3055 $rows = $readonlys = array(); 3056 $num_rows = $this->get_rows($query, $rows, $readonlys, true); 3057 $_GET['contact_id'] = array_shift($rows); 3058 $_GET['index'] = 0; 3059 } 3060 $contact_id = $_GET['contact_id'] ? $_GET['contact_id'] : ((int)$_GET['account_id'] ? 'account:'.(int)$_GET['account_id'] : 0); 3061 if(!$contact_id || !is_array($content = $this->read($contact_id))) 3062 { 3063 Egw::redirect_link('/index.php',array( 3064 'menuaction' => 'addressbook.addressbook_ui.index', 3065 'msg' => $content, 3066 'ajax' => 'true' 3067 )+(isset($_GET['search']) ? array('search' => $_GET['search']) : array())); 3068 } 3069 if (isset($_GET['index'])) 3070 { 3071 $content['index'] = (int)$_GET['index']; 3072 // get number of rows to determine if we can have a next button 3073 $query = Api\Cache::getSession('addressbook', 'index'); 3074 $query['start'] = $content['index']; 3075 $query['num_rows'] = 1; 3076 $rows = $readonlys = array(); 3077 $num_rows = $this->get_rows($query, $rows, $readonlys, true); 3078 } 3079 } 3080 $content['jpegphoto'] = !empty($content['jpegphoto']); // unused and messes up json encoding (not utf-8) 3081 3082 // make everything not explicit mentioned readonly 3083 $readonlys['__ALL__'] = true; 3084 $readonlys['photo'] = $readonlys['button[copy]'] =false; 3085 3086 foreach(array_keys($this->contact_fields) as $key) 3087 { 3088 if (in_array($key,array('tel_home','tel_work','tel_cell','tel_fax'))) 3089 { 3090 $content[$key.'2'] = $content[$key]; 3091 } 3092 } 3093 3094 // respect category permissions 3095 if(!empty($content['cat_id'])) 3096 { 3097 $content['cat_id'] = $this->categories->check_list(Acl::READ,$content['cat_id']); 3098 } 3099 $content['cat_id_tree'] = $content['cat_id']; 3100 3101 $content['view'] = true; 3102 $content['link_to'] = array( 3103 'to_app' => 'addressbook', 3104 'to_id' => $content['id'], 3105 ); 3106 // Links for deleted entries 3107 if($content['tid'] == self::DELETED_TYPE) 3108 { 3109 $content['link_to']['show_deleted'] = true; 3110 } 3111 $readonlys['button[delete]'] = !$content['owner'] || !$this->check_perms(Acl::DELETE,$content); 3112 $readonlys['button[edit]'] = !$this->check_perms(Acl::EDIT,$content); 3113 3114 // how to display addresses 3115 $content['addr_format'] = $this->addr_format_by_country($content['adr_one_countryname']); 3116 $content['addr_format2'] = $this->addr_format_by_country($content['adr_two_countryname']); 3117 3118 $sel_options['fileas_type'][$content['fileas_type']] = $this->fileas($content); 3119 $sel_options['owner'] = $this->get_addressbooks(); 3120 for($i = -23; $i<=23; $i++) 3121 { 3122 $tz[$i] = ($i > 0 ? '+' : '').$i; 3123 } 3124 $sel_options['tz'] = $tz; 3125 $content['tz'] = $content['tz'] ? $content['tz'] : 0; 3126 if (count($this->content_types) > 1) 3127 { 3128 foreach($this->content_types as $type => $data) 3129 { 3130 $sel_options['tid'][$type] = $data['name']; 3131 } 3132 $content['typegfx'] = Api\Html::image('addressbook',$this->content_types[$content['tid']]['options']['icon'],'',' width="16px" height="16px"'); 3133 } 3134 else 3135 { 3136 $content['no_tid'] = true; 3137 } 3138 $this->tmpl->read('addressbook.view'); 3139 /*if (!$this->tmpl->read($this->content_types[$content['tid']]['options']['template'] ? $this->content_types[$content['tid']]['options']['template'] : 'addressbook.edit')) 3140 { 3141 $content['msg'] = lang('WARNING: Template "%1" not found, using default template instead.', $this->content_types[$content['tid']]['options']['template'])."\n"; 3142 $content['msg'] .= lang('Please update the templatename in your customfields section!'); 3143 $this->tmpl->read('addressbook.edit'); 3144 }*/ 3145 if ($this->private_addressbook && $content['private'] && $content['owner'] == $this->user) 3146 { 3147 $content['owner'] .= 'p'; 3148 } 3149 $this->tmpl->set_cell_attribute('change_org','disabled',true); 3150 3151 // Prevent double countries - invalid code blanks it, disabling doesn't work 3152 $content['adr_one_countrycode'] = '-'; 3153 $content['adr_two_countrycode'] = '-'; 3154 3155 // Enable history 3156 $this->setup_history($content, $sel_options); 3157 3158 // disable not needed tabs 3159 $readonlys['tabs']['cats'] = !($content['cat_tab'] = $this->config['cat_tab']); 3160 $readonlys['tabs']['custom'] = !$this->customfields; 3161 $readonlys['tabs']['custom_private'] = !$this->customfields || !$this->config['private_cf_tab']; 3162 $readonlys['tabs']['distribution_list'] = !$content['distrib_lists'];#false; 3163 $readonlys['tabs']['history'] = $this->contact_repository != 'sql' || !$content['id'] || 3164 $this->account_repository != 'sql' && $content['account_id']; 3165 if ($this->config['private_cf_tab']) $content['no_private_cfs'] = 0; 3166 3167 // last and next calendar date 3168 if (!empty($content['id'])) $dates = current($this->read_calendar(array($content['account_id'] ? $content['account_id'] : 'c'.$content['id']),false)); 3169 if(is_array($dates)) $content += $dates; 3170 3171 // Disable importexport 3172 $GLOBALS['egw_info']['flags']['disable_importexport']['export'] = true; 3173 $GLOBALS['egw_info']['flags']['disable_importexport']['merge'] = true; 3174 3175 // set id for automatic linking via quick add 3176 $GLOBALS['egw_info']['flags']['currentid'] = $content['id']; 3177 3178 // load app.css for addressbook explicit, as addressbook_view hooks changes currentapp! 3179 Framework::includeCSS('addressbook', 'app'); 3180 3181 // dont show an app-header 3182 $GLOBALS['egw_info']['flags']['app_header'] = ''; 3183 3184 // always show sidebox, as it contains contact-data 3185 unset($GLOBALS['egw_info']['user']['preferences']['common']['auto_hide_sidebox']); 3186 3187 // need to load list's app.js now, as exec calls header before other app can include it 3188 // Framework::includeJS('/'.$crm_list.'/js/app.js'); 3189 3190 // Load CRM code 3191 Framework::includeJS('.','CRM','addressbook'); 3192 $content['view_sidebox'] = addressbook_hooks::getViewDOMID($contact_id, $crm_list); 3193 $this->tmpl->exec('addressbook.addressbook_ui.view',$content,$sel_options,$readonlys,array( 3194 'id' => $content['id'], 3195 'index' => $content['index'], 3196 'crm_list' => $crm_list 3197 )); 3198 3199 // Only load this on first time - we're using AJAX, so it stays there through submits. 3200 // Sending it again (via ajax) will break the addressbook.view etemplate2 3201 if($contact_id) 3202 { 3203 // Show for whole organisation, not just selected contact 3204 if($crm_list == 'infolog-organisation') 3205 { 3206 $crm_list = str_replace('-organisation','',$crm_list); 3207 $_query = Api\Cache::getSession('addressbook', 'index'); 3208 $content['id'] = $this->get_all_org_contacts($content['id']); 3209 } 3210 Api\Hooks::single(array( 3211 'location' => 'addressbook_view', 3212 'ab_id' => $content['id'] 3213 ),$crm_list); 3214 } 3215 } 3216 3217 /** 3218 * Get all the contact IDs in the given contact's organisation 3219 * 3220 * @param int $contact_id 3221 * @param Array $query Optional base query 3222 * 3223 * @return array of contact IDs in the organisation 3224 */ 3225 function get_all_org_contacts($contact_id, $query = array()) 3226 { 3227 $contact = $this->read($contact_id); 3228 3229 // No org name, early return with just the contact 3230 if(!$contact['org_name']) 3231 { 3232 return array($contact_id); 3233 } 3234 3235 $query['num_rows'] = -1; 3236 $query['start'] = 0; 3237 if(!array_key_exists('filter', $query)) 3238 { 3239 $query['filter'] = ''; 3240 } 3241 if(!is_array($query['col_filter'])) 3242 { 3243 $query['col_filter'] = array(); 3244 } 3245 $query['grouped_view'] = 'org_name:'.$contact['org_name']; 3246 3247 $org_contacts = array(); 3248 $readonlys = null; 3249 $this->get_rows($query,$org_contacts,$readonlys,true); // true = only return the id's 3250 3251 return $org_contacts ? $org_contacts : array($contact_id); 3252 } 3253 3254 /** 3255 * convert email-address in compose link 3256 * 3257 * @param string $email email-addresse 3258 * @return array/string array with get-params or mailto:$email, or '' or no mail addresse 3259 */ 3260 function email2link($email) 3261 { 3262 if (strpos($email,'@') == false) return ''; 3263 3264 if($GLOBALS['egw_info']['user']['apps']['mail']) 3265 { 3266 return array( 3267 'menuaction' => 'mail.mail_compose.compose', 3268 'send_to' => base64_encode($email) 3269 ); 3270 } 3271 if($GLOBALS['egw_info']['user']['apps']['felamimail']) 3272 { 3273 return array( 3274 'menuaction' => 'felamimail.uicompose.compose', 3275 'send_to' => base64_encode($email) 3276 ); 3277 } 3278 if($GLOBALS['egw_info']['user']['apps']['email']) 3279 { 3280 return array( 3281 'menuaction' => 'email.uicompose.compose', 3282 'to' => $email, 3283 ); 3284 } 3285 return 'mailto:' . $email; 3286 } 3287 3288 /** 3289 * Extended search 3290 * 3291 * @param array $_content 3292 * @return string 3293 */ 3294 function search($_content=array()) 3295 { 3296 if(!empty($_content)) 3297 { 3298 3299 $_content['cat_id'] = $this->config['cat_tab'] === 'Tree' ? $_content['cat_id_tree'] : $_content['cat_id']; 3300 3301 $response = Api\Json\Response::get(); 3302 3303 $query = Api\Cache::getSession('addressbook', 'index'); 3304 3305 if ($_content['button']['cancelsearch']) 3306 { 3307 unset($query['advanced_search']); 3308 } 3309 else 3310 { 3311 $query['advanced_search'] = array_intersect_key($_content,array_flip(array_merge($this->get_contact_columns(),array('operator','meth_select')))); 3312 foreach ($query['advanced_search'] as $key => $value) 3313 { 3314 if(!$value) unset($query['advanced_search'][$key]); 3315 } 3316 // Skip n_fn, it causes problems in sql 3317 unset($query['advanced_search']['n_fn']); 3318 } 3319 $query['search'] = ''; 3320 // store the index state in the session 3321 Api\Cache::setSession('addressbook', 'index', $query); 3322 3323 // store the advanced search in the session to call it again 3324 Api\Cache::setSession('addressbook', 'advanced_search', $query['advanced_search']); 3325 3326 // Update client / nextmatch with filters, or clear 3327 $response->call("app.addressbook.adv_search", array('advanced_search' => $_content['button']['search'] ? $query['advanced_search'] : '')); 3328 if ($_content['button']['cancelsearch']) 3329 { 3330 Framework::window_close (); 3331 3332 // No need to reload popup 3333 return; 3334 } 3335 } 3336 3337 $GLOBALS['egw_info']['etemplate']['advanced_search'] = true; 3338 3339 // initialize etemplate arrays 3340 $sel_options = $readonlys = array(); 3341 $this->tmpl->read('addressbook.edit'); 3342 $content = Api\Cache::getSession('addressbook', 'advanced_search'); 3343 $content['n_fn'] = $this->fullname($content); 3344 // Avoid ID conflict with tree & selectboxes 3345 $content['cat_id_tree'] = $content['cat_id']; 3346 3347 for($i = -23; $i<=23; $i++) 3348 { 3349 $tz[$i] = ($i > 0 ? '+' : '').$i; 3350 } 3351 $sel_options['tz'] = $tz + array('' => lang('doesn\'t matter')); 3352 $sel_options['tid'][] = lang('all'); 3353 //foreach($this->content_types as $type => $data) $sel_options['tid'][$type] = $data['name']; 3354 3355 // configure search options 3356 $sel_options['owner'] = $this->get_addressbooks(Acl::READ,lang('all')); 3357 $sel_options['operator'] = array( 3358 'AND' => lang('AND'), 3359 'OR' => lang('OR'), 3360 ); 3361 $sel_options['meth_select'] = array( 3362 '%' => lang('contains'), 3363 false => lang('exact'), 3364 ); 3365 if ($this->customfields) 3366 { 3367 foreach($this->customfields as $name => $data) 3368 { 3369 if (substr($data['type'], 0, 6) == 'select' && !($data['rows'] > 1)) 3370 { 3371 if (!isset($content['#'.$name])) $content['#'.$name] = ''; 3372 if(!isset($data['values'][''])) $sel_options['#'.$name][''] = lang('Select one'); 3373 } 3374 // Make them not required, otherwise you can't search 3375 $this->tmpl->setElementAttribute('#'.$name, 'needed', FALSE); 3376 } 3377 } 3378 // configure edit template as search dialog 3379 $readonlys['change_photo'] = true; 3380 $readonlys['fileas_type'] = true; 3381 $readonlys['creator'] = true; 3382 // this setting will enable (and show) the search and cancel buttons, setting this to true will hide the before mentioned buttons completely 3383 $readonlys['button'] = false; 3384 // disable not needed tabs 3385 $readonlys['tabs']['cats'] = !($content['cat_tab'] = $this->config['cat_tab']); 3386 $readonlys['tabs']['custom'] = !$this->customfields; 3387 $readonlys['tabs']['custom_private'] = !$this->customfields || !$this->config['private_cf_tab']; 3388 $readonlys['tabs']['links'] = true; 3389 $readonlys['tabs']['distribution_list'] = true; 3390 $readonlys['tabs']['history'] = true; 3391 // setting hidebuttons for content will hide the 'normal' addressbook edit dialog buttons 3392 $content['hidebuttons'] = true; 3393 $content['no_tid'] = true; 3394 $content['showsearchbuttons'] = true; // enable search operation and search buttons| they're disabled by default 3395 3396 if ($this->config['private_cf_tab']) $content['no_private_cfs'] = 0; 3397 3398 $this->tmpl->set_cell_attribute('change_org','disabled',true); 3399 return $this->tmpl->exec('addressbook.addressbook_ui.search',$content,$sel_options,$readonlys,array(),2); 3400 } 3401 3402 /** 3403 * Check if there's a photo for given contact id. This is used for avatar widget 3404 * to set or unset delete button. If there's no uploaded photo it responses true. 3405 * 3406 * @param type $contact_id 3407 */ 3408 function ajax_noPhotoExists ($contact_id) 3409 { 3410 $response = Api\Json\Response::get(); 3411 $response->data((!($contact = $this->read($contact_id)) || 3412 empty($contact['photo']) && !(($contact['files'] & Api\Contacts::FILES_BIT_PHOTO) && 3413 ($size = filesize($url=Api\Link::vfs_path('addressbook', $contact_id, Api\Contacts::FILES_PHOTO)))))); 3414 } 3415 3416 /** 3417 * Ajax method to update edited avatar photo via avatar widget 3418 * 3419 * @param string $etemplate_exec_id to update id, files, etag, ... 3420 * @param file string $file null means to delete 3421 */ 3422 function ajax_update_photo ($etemplate_exec_id, $file) 3423 { 3424 $et_request = Api\Etemplate\Request::read($etemplate_exec_id); 3425 $response = Api\Json\Response::get(); 3426 if ($file) 3427 { 3428 $filteredFile = substr($file, strpos($file, ",")+1); 3429 // resize photo if wider then default width of 240pixel (keeping aspect ratio) 3430 $decoded = $this->resize_photo(base64_decode($filteredFile)); 3431 } 3432 $response->data(true); 3433 // add photo into current eT2 request 3434 $et_request->preserv = array_merge($et_request->preserv, array( 3435 'jpegphoto' => is_null($file) ? $file : $decoded, 3436 'photo_unchanged' => false, // hint photo is changed 3437 )); 3438 } 3439 3440 /** 3441 * Callback for vfs-upload widgets for PGP and S/Mime pubkey 3442 * 3443 * @param array $file 3444 * @param string $widget_id 3445 * @param Api\Etemplate\Request $request eT2 request eg. to access attribute $content 3446 * @param Api\Json\Response $response 3447 */ 3448 public function pubkey_uploaded(array $file, $widget_id, Api\Etemplate\Request $request, Api\Json\Response $response) 3449 { 3450 //error_log(__METHOD__."(".array2string($file).", ...) widget_id=$widget_id, id=".$request->content['id'].", files=".$request->content['files']); 3451 unset($file, $response); // not used, but required by function signature 3452 list(,,$path) = explode(':', $widget_id); 3453 $bit = $path === Api\Contacts::FILES_PGP_PUBKEY ? Api\Contacts::FILES_BIT_PGP_PUBKEY : Api\Contacts::FILES_BIT_SMIME_PUBKEY; 3454 if (!($request->content['files'] & $bit) && $this->check_perms(Acl::EDIT, $request->content)) 3455 { 3456 $content = $request->content; 3457 $content['files'] |= $bit; 3458 $content['photo_unchanged'] = true; // hint no need to store photo 3459 if ($this->save($content)) 3460 { 3461 $changed = array_diff_assoc($content, $request->content); 3462 //error_log(__METHOD__."() changed=".array2string($changed)); 3463 $request->content = $content; 3464 // need to update preserv, as edit stores content there too and we would get eg. an contact modified error when trying to store 3465 $request->preserv = array_merge($request->preserv, $changed); 3466 } 3467 } 3468 } 3469 3470 /** 3471 * Migrate contacts to or from LDAP (called by Admin >> Addressbook >> Site configuration (Admin only) 3472 * 3473 */ 3474 function migrate2ldap() 3475 { 3476 $GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Migration to LDAP'); 3477 echo $GLOBALS['egw']->framework->header(); 3478 echo $GLOBALS['egw']->framework->navbar(); 3479 3480 if (!$this->is_admin()) 3481 { 3482 echo '<h1>'.lang('Permission denied !!!')."</h1>\n"; 3483 } 3484 else 3485 { 3486 parent::migrate2ldap($_GET['type']); 3487 echo '<p style="margin-top: 20px;"><b>'.lang('Migration finished')."</b></p>\n"; 3488 } 3489 echo $GLOBALS['egw']->framework->footer(); 3490 } 3491 3492 /** 3493 * Set n_fileas (and n_fn) in contacts of all users (called by Admin >> Addressbook >> Site configuration (Admin only) 3494 * 3495 * If $_GET[all] all fileas fields will be set, if !$_GET[all] only empty ones 3496 * 3497 */ 3498 function admin_set_fileas() 3499 { 3500 Api\Translation::add_app('admin'); 3501 $GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Contact maintenance'); 3502 echo $GLOBALS['egw']->framework->header(); 3503 echo $GLOBALS['egw']->framework->navbar(); 3504 3505 // check if user has admin rights AND if a valid fileas type is given (Security) 3506 if (!$this->is_admin() || $_GET['type'] != '' && !in_array($_GET['type'],$this->fileas_types)) 3507 { 3508 echo '<h1>'.lang('Permission denied !!!')."</h1>\n"; 3509 } 3510 else 3511 { 3512 $errors = null; 3513 $updated = parent::set_all_fileas($_GET['type'],(boolean)$_GET['all'],$errors,true); // true = ignore Acl 3514 echo '<p style="margin-top: 20px;"><b>'.lang('%1 contacts updated (%2 errors).',$updated,$errors)."</b></p>\n"; 3515 } 3516 echo $GLOBALS['egw']->framework->footer(); 3517 } 3518 3519 /** 3520 * Cleanup all contacts of all users (called by Admin >> Addressbook >> Site configuration (Admin only) 3521 * 3522 */ 3523 function admin_set_all_cleanup() 3524 { 3525 Api\Translation::add_app('admin'); 3526 $GLOBALS['egw_info']['flags']['app_header'] = lang('Addressbook').' - '.lang('Contact maintenance'); 3527 echo $GLOBALS['egw']->framework->header(); 3528 echo $GLOBALS['egw']->framework->navbar(); 3529 3530 // check if user has admin rights (Security) 3531 if (!$this->is_admin()) 3532 { 3533 echo '<h1>'.lang('Permission denied !!!')."</h1>\n"; 3534 } 3535 else 3536 { 3537 $errors = null; 3538 $updated = parent::set_all_cleanup($errors,true); // true = ignore Acl 3539 echo '<p style="margin-top: 20px;"><b>'.lang('%1 contacts updated (%2 errors).',$updated,$errors)."</b></p>\n"; 3540 } 3541 echo $GLOBALS['egw']->framework->footer(); 3542 } 3543 3544 /** 3545 * Set up history log widget 3546 */ 3547 protected function setup_history(&$content, &$sel_options) 3548 { 3549 if ($this->contact_repository == 'ldap' || !$content['id'] || 3550 $this->account_repository == 'ldap' && $content['account_id']) 3551 { 3552 return; // no history for ldap as history table only allows integer id's 3553 } 3554 $content['history'] = array( 3555 'id' => $content['id'], 3556 'app' => 'addressbook', 3557 'status-widgets' => array( 3558 'owner' => 'select-account', 3559 'creator' => 'select-account', 3560 'created' => 'date-time', 3561 'cat_id' => 'select-cat', 3562 'adr_one_countrycode' => 'select-country', 3563 'adr_two_countrycode' => 'select-country', 3564 ), 3565 ); 3566 3567 foreach($this->content_types as $id => $settings) 3568 { 3569 $content['history']['status-widgets']['tid'][$id] = $settings['name']; 3570 } 3571 $sel_options['status'] = $this->contact_fields; 3572 3573 // custom fields no longer need to be added, historylog-widget "knows" about them 3574 } 3575} 3576