diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index 3959bfada..ddc0e765a 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -1,179 +1,181 @@ | | Author: Aleksander Machniak | +-----------------------------------------------------------------------+ */ $RCMAIL->request_security_check(rcube_utils::INPUT_GET); // Use search result -if (!empty($_REQUEST['_search']) && isset($_SESSION['search'][$_REQUEST['_search']])) { +if (!empty($_REQUEST['_search']) && isset($_SESSION['contact_search'][$_REQUEST['_search']]) + && is_array($_SESSION['contact_search'][$_REQUEST['_search']]) +) { $sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name'); - $search = (array)$_SESSION['search'][$_REQUEST['_search']]; + $search = $_SESSION['contact_search'][$_REQUEST['_search']]; $records = array(); // Get records from all sources foreach ($search as $s => $set) { $source = $RCMAIL->get_address_book($s); // reset page $source->set_page(1); $source->set_pagesize(99999); $source->set_search_set($set); // get records $result = $source->list_records(); while ($record = $result->next()) { // because vcard_map is per-source we need to create vcard here prepare_for_export($record, $source); $record['sourceid'] = $s; $key = rcube_addressbook::compose_contact_key($record, $sort_col); $records[$key] = $record; } unset($result); } // sort the records ksort($records, SORT_LOCALE_STRING); // create resultset object $count = count($records); $result = new rcube_result_set($count); $result->records = array_values($records); } // selected contacts else if (!empty($_REQUEST['_cid'])) { $sort_col = $RCMAIL->config->get('addressbook_sort_col', 'name'); $records = array(); // Selected contact IDs (with multi-source support) $cids = rcmail_get_cids(); foreach ($cids as $s => $ids) { $source = $RCMAIL->get_address_book($s); // reset page and page size (#6103) $source->set_page(1); $source->set_pagesize(count($ids)); $result = $source->search('ID', $ids, 1, true, true); while ($record = $result->next()) { // because vcard_map is per-source we need to create vcard here prepare_for_export($record, $source); $record['sourceid'] = $s; $key = rcube_addressbook::compose_contact_key($record, $sort_col); $records[$key] = $record; } } ksort($records, SORT_LOCALE_STRING); // create resultset object $count = count($records); $result = new rcube_result_set($count); $result->records = array_values($records); } // selected directory/group else { $CONTACTS = rcmail_contact_source(null, true); // get contacts for this user $CONTACTS->set_page(1); $CONTACTS->set_pagesize(99999); $result = $CONTACTS->list_records(null, 0, true); } // Give plugins a possibility to implement other output formats or modify the result $plugin = $RCMAIL->plugins->exec_hook('addressbook_export', array('result' => $result)); $result = $plugin['result']; if ($plugin['abort']) { exit; } // send downlaod headers header('Content-Type: text/vcard; charset=' . RCUBE_CHARSET); header('Content-Disposition: attachment; filename="contacts.vcf"'); while ($result && ($row = $result->next())) { if ($CONTACTS) { prepare_for_export($row, $CONTACTS); } // fix folding and end-of-line chars $row['vcard'] = preg_replace('/\r|\n\s+/', '', $row['vcard']); $row['vcard'] = preg_replace('/\n/', rcube_vcard::$eol, $row['vcard']); echo rcube_vcard::rfc2425_fold($row['vcard']) . rcube_vcard::$eol; } exit; /** * Copy contact record properties into a vcard object */ function prepare_for_export(&$record, $source = null) { $groups = $source && $source->groups && $source->export_groups ? $source->get_record_groups($record['ID']) : null; $fieldmap = $source ? $source->vcard_map : null; if (empty($record['vcard'])) { $vcard = new rcube_vcard($record['vcard'], RCUBE_CHARSET, false, $fieldmap); $vcard->reset(); foreach ($record as $key => $values) { list($field, $section) = explode(':', $key); // avoid unwanted casting of DateTime objects to an array // (same as in rcube_contacts::convert_save_data()) if (is_object($values) && is_a($values, 'DateTime')) { $values = array($values); } foreach ((array) $values as $value) { if (is_array($value) || is_a($value, 'DateTime') || @strlen($value)) { $vcard->set($field, $value, strtoupper($section)); } } } // append group names if ($groups) { $vcard->set('groups', join(',', $groups), null); } $record['vcard'] = $vcard->export(); } // patch categories to alread existing vcard block else if ($record['vcard']) { $vcard = new rcube_vcard($record['vcard'], RCUBE_CHARSET, false, $fieldmap); // unset CATEGORIES entry, it might be not up-to-date (#1490277) $vcard->set('groups', null); $record['vcard'] = $vcard->export(); if (!empty($groups)) { $vgroups = 'CATEGORIES:' . rcube_vcard::vcard_quote($groups, ','); $record['vcard'] = str_replace('END:VCARD', $vgroups . rcube_vcard::$eol . 'END:VCARD', $record['vcard']); } } } diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 8cbf8af43..cdc8a0e67 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -1,1078 +1,1080 @@ | +-----------------------------------------------------------------------+ */ $SEARCH_MODS_DEFAULT = array('name'=>1, 'firstname'=>1, 'surname'=>1, 'email'=>1, '*'=>1); // general definition of contact coltypes $CONTACT_COLTYPES = array( 'name' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('name'), 'category' => 'main'), 'firstname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('firstname'), 'category' => 'main'), 'surname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('surname'), 'category' => 'main'), 'email' => array('type' => 'text', 'size' => 40, 'maxlength' => 254, 'label' => $RCMAIL->gettext('email'), 'subtypes' => array('home','work','other'), 'category' => 'main'), 'middlename' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('middlename'), 'category' => 'main'), 'prefix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $RCMAIL->gettext('nameprefix'), 'category' => 'main'), 'suffix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $RCMAIL->gettext('namesuffix'), 'category' => 'main'), 'nickname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('nickname'), 'category' => 'main'), 'jobtitle' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'limit' => 1, 'label' => $RCMAIL->gettext('jobtitle'), 'category' => 'main'), 'organization' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'limit' => 1, 'label' => $RCMAIL->gettext('organization'), 'category' => 'main'), 'department' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'limit' => 1, 'label' => $RCMAIL->gettext('department'), 'category' => 'main'), 'gender' => array('type' => 'select', 'limit' => 1, 'label' => $RCMAIL->gettext('gender'), 'options' => array('male' => $RCMAIL->gettext('male'), 'female' => $RCMAIL->gettext('female')), 'category' => 'personal'), 'maidenname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $RCMAIL->gettext('maidenname'), 'category' => 'personal'), 'phone' => array('type' => 'text', 'size' => 40, 'maxlength' => 20, 'label' => $RCMAIL->gettext('phone'), 'subtypes' => array('home','home2','work','work2','mobile','main','homefax','workfax','car','pager','video','assistant','other'), 'category' => 'main'), 'address' => array('type' => 'composite', 'label' => $RCMAIL->gettext('address'), 'subtypes' => array('home','work','other'), 'childs' => array( 'street' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $RCMAIL->gettext('street'), 'category' => 'main'), 'locality' => array('type' => 'text', 'size' => 28, 'maxlength' => 50, 'label' => $RCMAIL->gettext('locality'), 'category' => 'main'), 'zipcode' => array('type' => 'text', 'size' => 8, 'maxlength' => 15, 'label' => $RCMAIL->gettext('zipcode'), 'category' => 'main'), 'region' => array('type' => 'text', 'size' => 12, 'maxlength' => 50, 'label' => $RCMAIL->gettext('region'), 'category' => 'main'), 'country' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $RCMAIL->gettext('country'), 'category' => 'main'), ), 'category' => 'main'), 'birthday' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $RCMAIL->gettext('birthday'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'), 'anniversary' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $RCMAIL->gettext('anniversary'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'), 'website' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'label' => $RCMAIL->gettext('website'), 'subtypes' => array('homepage','work','blog','profile','other'), 'category' => 'main'), 'im' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'label' => $RCMAIL->gettext('instantmessenger'), 'subtypes' => array('aim','icq','msn','yahoo','jabber','skype','other'), 'category' => 'main'), 'notes' => array('type' => 'textarea', 'size' => 40, 'rows' => 15, 'maxlength' => 500, 'label' => $RCMAIL->gettext('notes'), 'limit' => 1), 'photo' => array('type' => 'image', 'limit' => 1, 'category' => 'main'), 'assistant' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'limit' => 1, 'label' => $RCMAIL->gettext('assistant'), 'category' => 'personal'), 'manager' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'limit' => 1, 'label' => $RCMAIL->gettext('manager'), 'category' => 'personal'), 'spouse' => array('type' => 'text', 'size' => 40, 'maxlength' => 128, 'limit' => 1, 'label' => $RCMAIL->gettext('spouse'), 'category' => 'personal'), // TODO: define fields for vcards like GEO, KEY ); $PAGE_SIZE = $RCMAIL->config->get('addressbook_pagesize', $RCMAIL->config->get('pagesize', 50)); // Addressbook UI if (!$RCMAIL->action && !$OUTPUT->ajax_call) { // add list of address sources to client env $js_list = $RCMAIL->get_address_sources(); // count all/writeable sources $writeable = 0; $count = 0; foreach ($js_list as $sid => $s) { $count++; if (!$s['readonly']) { $writeable++; } // unset hidden sources if ($s['hidden']) { unset($js_list[$sid]); } } $OUTPUT->set_env('display_next', (bool) $RCMAIL->config->get('display_next')); $OUTPUT->set_env('search_mods', $RCMAIL->config->get('addressbook_search_mods', $SEARCH_MODS_DEFAULT)); $OUTPUT->set_env('address_sources', $js_list); $OUTPUT->set_env('writable_source', $writeable); $OUTPUT->set_env('contact_move_enabled', $writeable > 1); $OUTPUT->set_env('contact_copy_enabled', $writeable > 1 || ($writeable == 1 && count($js_list) > 1)); $OUTPUT->set_pagetitle($RCMAIL->gettext('contacts')); $_SESSION['addressbooks_count'] = $count; $_SESSION['addressbooks_count_writeable'] = $writeable; // select address book $source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); // use first directory by default if (!strlen($source) || !isset($js_list[$source])) { $source = $RCMAIL->config->get('default_addressbook'); if (!strlen($source) || !isset($js_list[$source])) { $source = strval(key($js_list)); } } $CONTACTS = rcmail_contact_source($source, true); } // remove undo information... if ($undo = $_SESSION['contact_undo']) { // ...after timeout $undo_time = $RCMAIL->config->get('undo_timeout', 0); if ($undo['ts'] < time() - $undo_time) $RCMAIL->session->remove('contact_undo'); } // register UI objects $OUTPUT->add_handlers(array( 'directorylist' => 'rcmail_directory_list', 'savedsearchlist' => 'rcmail_savedsearch_list', 'addresslist' => 'rcmail_contacts_list', 'addresslisttitle' => 'rcmail_contacts_list_title', 'recordscountdisplay' => 'rcmail_rowcount_display', 'searchform' => array($OUTPUT, 'search_form') )); // register action aliases $RCMAIL->register_action_map(array( 'add' => 'edit.inc', 'group-create' => 'groups.inc', 'group-rename' => 'groups.inc', 'group-delete' => 'groups.inc', 'group-addmembers' => 'groups.inc', 'group-delmembers' => 'groups.inc', 'search-create' => 'search.inc', 'search-delete' => 'search.inc', )); // Disable qr-code if php-gd or Endroid's QrCode is not installed if (!$OUTPUT->ajax_call) { $OUTPUT->set_env('qrcode', function_exists('imagecreate') && class_exists('Endroid\QrCode\QrCode')); $OUTPUT->add_label('qrcode'); } // instantiate a contacts object according to the given source function rcmail_contact_source($source=null, $init_env=false, $writable=false) { global $RCMAIL, $OUTPUT, $CONTACT_COLTYPES, $PAGE_SIZE; if (!strlen($source)) { $source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); } // Get object $CONTACTS = $RCMAIL->get_address_book($source, $writable); $CONTACTS->set_pagesize($PAGE_SIZE); // set list properties and session vars if (!empty($_GET['_page'])) $CONTACTS->set_page(($_SESSION['page'] = intval($_GET['_page']))); else $CONTACTS->set_page(isset($_SESSION['page']) ? $_SESSION['page'] : 1); if ($group = rcube_utils::get_input_value('_gid', rcube_utils::INPUT_GP)) { $CONTACTS->set_group($group); } if (!$init_env) { return $CONTACTS; } $OUTPUT->set_env('readonly', $CONTACTS->readonly); $OUTPUT->set_env('source', (string) $source); $OUTPUT->set_env('group', $group); // reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object if (is_array($CONTACTS->coltypes)) { // remove cols not listed by the backend class $contact_cols = $CONTACTS->coltypes[0] ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes; $CONTACT_COLTYPES = array_intersect_key($CONTACT_COLTYPES, $contact_cols); // add associative coltypes definition if (!$CONTACTS->coltypes[0]) { foreach ($CONTACTS->coltypes as $col => $colprop) { if (is_array($colprop['childs'])) { foreach ($colprop['childs'] as $childcol => $childprop) $colprop['childs'][$childcol] = array_merge((array)$CONTACT_COLTYPES[$col]['childs'][$childcol], $childprop); } $CONTACT_COLTYPES[$col] = $CONTACT_COLTYPES[$col] ? array_merge($CONTACT_COLTYPES[$col], $colprop) : $colprop; } } } $OUTPUT->set_env('photocol', is_array($CONTACT_COLTYPES['photo'])); return $CONTACTS; } function rcmail_set_sourcename($abook) { global $OUTPUT, $RCMAIL; // get address book name (for display) if ($abook && $_SESSION['addressbooks_count'] > 1) { $name = $abook->get_name(); if (!$name) { $name = $RCMAIL->gettext('personaladrbook'); } $OUTPUT->set_env('sourcename', html_entity_decode($name, ENT_COMPAT, 'UTF-8')); } } function rcmail_directory_list($attrib) { global $RCMAIL, $OUTPUT; if (!$attrib['id']) $attrib['id'] = 'rcmdirectorylist'; $out = ''; $jsdata = array(); $line_templ = html::tag('li', array( 'id' => 'rcmli%s', 'class' => '%s', 'noclose' => true), html::a(array('href' => '%s', 'rel' => '%s', 'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('list','%s',this)"), '%s')); $sources = (array) $OUTPUT->get_env('address_sources'); reset($sources); // currently selected source $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC); foreach ($sources as $j => $source) { $id = strval(strlen($source['id']) ? $source['id'] : $j); $js_id = rcube::JQ($id); // set class name(s) $class_name = 'addressbook'; if ($current === $id) $class_name .= ' selected'; if ($source['readonly']) $class_name .= ' readonly'; if ($source['class_name']) $class_name .= ' ' . $source['class_name']; $name = $source['name'] ?: $id; $out .= sprintf($line_templ, rcube_utils::html_identifier($id, true), $class_name, rcube::Q($RCMAIL->url(array('_source' => $id))), $source['id'], $js_id, $name); $groupdata = array('out' => $out, 'jsdata' => $jsdata, 'source' => $id); if ($source['groups']) $groupdata = rcmail_contact_groups($groupdata); $jsdata = $groupdata['jsdata']; $out = $groupdata['out']; $out .= ''; } $OUTPUT->set_env('contactgroups', $jsdata); $OUTPUT->set_env('collapsed_abooks', (string)$RCMAIL->config->get('collapsed_abooks','')); $OUTPUT->add_gui_object('folderlist', $attrib['id']); $OUTPUT->include_script('treelist.js'); // add some labels to client $OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember', 'newgroup', 'grouprename', 'searchsave', 'namex', 'save', 'import', 'importcontacts', 'advsearch', 'search' ); return html::tag('ul', $attrib, $out, html::$common_attrib); } function rcmail_savedsearch_list($attrib) { global $RCMAIL, $OUTPUT; if (!$attrib['id']) $attrib['id'] = 'rcmsavedsearchlist'; $out = ''; $line_templ = html::tag('li', array( 'id' => 'rcmli%s', 'class' => '%s'), html::a(array('href' => '#', 'rel' => 'S%s', 'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s')); // Saved searches $sources = $RCMAIL->user->list_searches(rcube_user::SEARCH_ADDRESSBOOK); foreach ($sources as $source) { $id = $source['id']; $js_id = rcube::JQ($id); // set class name(s) $classes = array('contactsearch'); if (!empty($source['class_name'])) $classes[] = $source['class_name']; $out .= sprintf($line_templ, rcube_utils::html_identifier('S'.$id, true), join(' ', $classes), $id, $js_id, rcube::Q($source['name'] ?: $id) ); } $OUTPUT->add_gui_object('savedsearchlist', $attrib['id']); return html::tag('ul', $attrib, $out, html::$common_attrib); } function rcmail_contact_groups($args) { global $RCMAIL; $groups_html = ''; $groups = $RCMAIL->get_address_book($args['source'])->list_groups(); if (!empty($groups)) { $line_templ = html::tag('li', array( 'id' => 'rcmli%s', 'class' => 'contactgroup'), html::a(array('href' => '#', 'rel' => '%s:%s', 'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)"), '%s')); // append collapse/expand toggle and open a new