mirror of
https://github.com/roundcube/roundcubemail.git
synced 2026-02-20 01:21:20 +01:00
* contact import: correct mismapped fields * contacts: remove im:other field from UI, it does not exist in the vCard * vcard: add some more maps for common vcard types to roundcube types * contact import: list all possible roundcube contact fields in csv import UI, remove hard coded $local_map * add SORT_LOCALE_STRING flag * fix typos * remove unwanted label * move field list to csv2vcard * move rcube_csv2vcard::list_fields to rcmail_action_contacts_import::list_fields as it relies on rcmail_action_contacts * use single field map for csv2vcard imports, remove hardcoded version * fix test * small cs fix * reformat csv2vcard.inc * fix failing test * restore existance check * fix failing test again
1496 lines
56 KiB
PHP
1496 lines
56 KiB
PHP
<?php
|
|
|
|
/*
|
|
+-----------------------------------------------------------------------+
|
|
| This file is part of the Roundcube Webmail client |
|
|
| |
|
|
| Copyright (C) The Roundcube Dev Team |
|
|
| |
|
|
| Licensed under the GNU General Public License version 3 or |
|
|
| any later version with exceptions for skins & plugins. |
|
|
| See the README file for a full license statement. |
|
|
| |
|
|
| PURPOSE: |
|
|
| Provide addressbook functionality and GUI objects |
|
|
+-----------------------------------------------------------------------+
|
|
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
|
+-----------------------------------------------------------------------+
|
|
*/
|
|
|
|
class rcmail_action_contacts_index extends rcmail_action
|
|
{
|
|
public static $aliases = [
|
|
'add' => 'edit',
|
|
];
|
|
|
|
protected static $SEARCH_MODS_DEFAULT = [
|
|
'name' => 1,
|
|
'firstname' => 1,
|
|
'surname' => 1,
|
|
'email' => 1,
|
|
'*' => 1,
|
|
];
|
|
|
|
/**
|
|
* General definition of contact coltypes
|
|
*/
|
|
public static $CONTACT_COLTYPES = [
|
|
'name' => [
|
|
'size' => 40,
|
|
'maxlength' => 50,
|
|
'limit' => 1,
|
|
'label' => 'name',
|
|
'category' => 'main',
|
|
],
|
|
'firstname' => [
|
|
'size' => 19,
|
|
'maxlength' => 50,
|
|
'limit' => 1,
|
|
'label' => 'firstname',
|
|
'category' => 'main',
|
|
],
|
|
'surname' => [
|
|
'size' => 19,
|
|
'maxlength' => 50,
|
|
'limit' => 1,
|
|
'label' => 'surname',
|
|
'category' => 'main',
|
|
],
|
|
'email' => [
|
|
'size' => 40,
|
|
'maxlength' => 254,
|
|
'label' => 'email',
|
|
'subtypes' => ['home', 'work', 'other'],
|
|
'category' => 'main',
|
|
],
|
|
'middlename' => [
|
|
'size' => 19,
|
|
'maxlength' => 50,
|
|
'limit' => 1,
|
|
'label' => 'middlename',
|
|
'category' => 'main',
|
|
],
|
|
'prefix' => [
|
|
'size' => 8,
|
|
'maxlength' => 20,
|
|
'limit' => 1,
|
|
'label' => 'nameprefix',
|
|
'category' => 'main',
|
|
],
|
|
'suffix' => [
|
|
'size' => 8,
|
|
'maxlength' => 20,
|
|
'limit' => 1,
|
|
'label' => 'namesuffix',
|
|
'category' => 'main',
|
|
],
|
|
'nickname' => [
|
|
'size' => 40,
|
|
'maxlength' => 50,
|
|
'limit' => 1,
|
|
'label' => 'nickname',
|
|
'category' => 'main',
|
|
],
|
|
'jobtitle' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'limit' => 1,
|
|
'label' => 'jobtitle',
|
|
'category' => 'main',
|
|
],
|
|
'organization' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'limit' => 1,
|
|
'label' => 'organization',
|
|
'category' => 'main',
|
|
],
|
|
'department' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'limit' => 1,
|
|
'label' => 'department',
|
|
'category' => 'main',
|
|
],
|
|
'gender' => [
|
|
'type' => 'select',
|
|
'limit' => 1,
|
|
'label' => 'gender',
|
|
'category' => 'personal',
|
|
'options' => [
|
|
'male' => 'male',
|
|
'female' => 'female',
|
|
'non-binary' => 'nonbinary',
|
|
'other' => 'othergender',
|
|
],
|
|
],
|
|
'maidenname' => [
|
|
'size' => 40,
|
|
'maxlength' => 50,
|
|
'limit' => 1,
|
|
'label' => 'maidenname',
|
|
'category' => 'personal',
|
|
],
|
|
'phone' => [
|
|
'size' => 40,
|
|
'maxlength' => 20,
|
|
'label' => 'phone',
|
|
'category' => 'main',
|
|
'subtypes' => ['home', 'home2', 'work', 'work2', 'mobile', 'main', 'homefax', 'workfax', 'car',
|
|
'pager', 'video', 'assistant', 'other'],
|
|
],
|
|
'address' => [
|
|
'type' => 'composite',
|
|
'label' => 'address',
|
|
'subtypes' => ['home', 'work', 'other'],
|
|
'category' => 'main',
|
|
'childs' => [
|
|
'street' => [
|
|
'label' => 'street',
|
|
'size' => 40,
|
|
'maxlength' => 50,
|
|
],
|
|
'locality' => [
|
|
'label' => 'locality',
|
|
'size' => 28,
|
|
'maxlength' => 50,
|
|
],
|
|
'zipcode' => [
|
|
'label' => 'zipcode',
|
|
'size' => 8,
|
|
'maxlength' => 15,
|
|
],
|
|
'region' => [
|
|
'label' => 'region',
|
|
'size' => 12,
|
|
'maxlength' => 50,
|
|
],
|
|
'country' => [
|
|
'label' => 'country',
|
|
'size' => 40,
|
|
'maxlength' => 50,
|
|
],
|
|
],
|
|
],
|
|
'birthday' => [
|
|
'type' => 'date',
|
|
'size' => 12,
|
|
'maxlength' => 16,
|
|
'label' => 'birthday',
|
|
'limit' => 1,
|
|
'render_func' => 'rcmail_action_contacts_index::format_date_col',
|
|
'category' => 'personal',
|
|
],
|
|
'anniversary' => [
|
|
'type' => 'date',
|
|
'size' => 12,
|
|
'maxlength' => 16,
|
|
'label' => 'anniversary',
|
|
'limit' => 1,
|
|
'render_func' => 'rcmail_action_contacts_index::format_date_col',
|
|
'category' => 'personal',
|
|
],
|
|
'website' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'label' => 'website',
|
|
'subtypes' => ['homepage', 'work', 'blog', 'profile', 'other'],
|
|
'category' => 'main',
|
|
],
|
|
'im' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'label' => 'instantmessenger',
|
|
'subtypes' => ['aim', 'icq', 'msn', 'yahoo', 'jabber', 'skype'],
|
|
'category' => 'main',
|
|
],
|
|
'notes' => [
|
|
'type' => 'textarea',
|
|
'size' => 40,
|
|
'rows' => 15,
|
|
'maxlength' => 500,
|
|
'label' => 'notes',
|
|
'limit' => 1,
|
|
],
|
|
'photo' => [
|
|
'type' => 'image',
|
|
'limit' => 1,
|
|
'category' => 'main',
|
|
],
|
|
'assistant' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'limit' => 1,
|
|
'label' => 'assistant',
|
|
'category' => 'personal',
|
|
],
|
|
'manager' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'limit' => 1,
|
|
'label' => 'manager',
|
|
'category' => 'personal',
|
|
],
|
|
'spouse' => [
|
|
'size' => 40,
|
|
'maxlength' => 128,
|
|
'limit' => 1,
|
|
'label' => 'spouse',
|
|
'category' => 'personal',
|
|
],
|
|
];
|
|
|
|
protected static $CONTACTS;
|
|
protected static $SOURCE_ID;
|
|
protected static $contact;
|
|
|
|
/**
|
|
* Request handler.
|
|
*
|
|
* @param array $args Arguments from the previous step(s)
|
|
*/
|
|
#[\Override]
|
|
public function run($args = [])
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// Prepare coltypes
|
|
foreach (self::$CONTACT_COLTYPES as $idx => $val) {
|
|
if (!empty($val['label'])) {
|
|
self::$CONTACT_COLTYPES[$idx]['label'] = $rcmail->gettext($val['label']);
|
|
}
|
|
if (!empty($val['options'])) {
|
|
foreach ($val['options'] as $i => $v) {
|
|
self::$CONTACT_COLTYPES[$idx]['options'][$i] = $rcmail->gettext($v);
|
|
}
|
|
}
|
|
if (!empty($val['childs'])) {
|
|
foreach ($val['childs'] as $i => $v) {
|
|
self::$CONTACT_COLTYPES[$idx]['childs'][$i]['label'] = $rcmail->gettext($v['label']);
|
|
if (empty($v['type'])) {
|
|
self::$CONTACT_COLTYPES[$idx]['childs'][$i]['type'] = 'text';
|
|
}
|
|
}
|
|
}
|
|
if (empty($val['type'])) {
|
|
self::$CONTACT_COLTYPES[$idx]['type'] = 'text';
|
|
}
|
|
}
|
|
|
|
// Addressbook UI
|
|
if (!$rcmail->action && !$rcmail->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 (!empty($s['hidden'])) {
|
|
unset($js_list[$sid]);
|
|
}
|
|
}
|
|
|
|
$rcmail->output->set_env('display_next', (bool) $rcmail->config->get('display_next'));
|
|
$rcmail->output->set_env('search_mods', $rcmail->config->get('addressbook_search_mods', self::$SEARCH_MODS_DEFAULT));
|
|
$rcmail->output->set_env('address_sources', $js_list);
|
|
$rcmail->output->set_env('writable_source', $writeable);
|
|
$rcmail->output->set_env('contact_move_enabled', $writeable > 1);
|
|
$rcmail->output->set_env('contact_copy_enabled', $writeable > 1 || ($writeable == 1 && count($js_list) > 1));
|
|
|
|
$rcmail->output->set_pagetitle($rcmail->gettext('contacts'));
|
|
|
|
$_SESSION['addressbooks_count'] = $count;
|
|
$_SESSION['addressbooks_count_writeable'] = $writeable;
|
|
|
|
// select address book
|
|
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
|
|
|
|
// use first directory by default
|
|
if (!strlen($source) || !isset($js_list[$source])) {
|
|
$source = $rcmail->config->get('default_addressbook');
|
|
if (!is_string($source) || !strlen($source) || !isset($js_list[$source])) {
|
|
$source = strval(key($js_list));
|
|
}
|
|
}
|
|
|
|
self::$CONTACTS = self::contact_source($source, true);
|
|
}
|
|
|
|
// remove undo information...
|
|
if (!empty($_SESSION['contact_undo'])) {
|
|
// ...after timeout
|
|
$undo = $_SESSION['contact_undo'];
|
|
$undo_time = $rcmail->config->get('undo_timeout', 0);
|
|
if ($undo['ts'] < time() - $undo_time) {
|
|
$rcmail->session->remove('contact_undo');
|
|
}
|
|
}
|
|
|
|
// register UI objects
|
|
$rcmail->output->add_handlers([
|
|
'directorylist' => [$this, 'directory_list'],
|
|
'savedsearchlist' => [$this, 'savedsearch_list'],
|
|
'addresslist' => [$this, 'contacts_list'],
|
|
'addresslisttitle' => [$this, 'contacts_list_title'],
|
|
'recordscountdisplay' => [$this, 'rowcount_display'],
|
|
'searchform' => [$rcmail->output, 'search_form'],
|
|
]);
|
|
|
|
// Disable qr-code if imagick, iconv or BaconQrCode is not installed
|
|
if (!$rcmail->output->ajax_call && rcmail_action_contacts_qrcode::check_support()) {
|
|
$rcmail->output->set_env('qrcode', true);
|
|
$rcmail->output->add_label('qrcode');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Instantiate a contacts object according to the given source
|
|
*
|
|
* @return rcube_addressbook|null Contacts handler
|
|
*/
|
|
public static function contact_source($source = null, $init_env = false, $writable = false)
|
|
{
|
|
if ($source === null || !strlen((string) $source)) {
|
|
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50));
|
|
|
|
// Get object
|
|
$contacts = $rcmail->get_address_book($source, $writable);
|
|
|
|
if (!$contacts) {
|
|
return null;
|
|
}
|
|
|
|
$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($_SESSION['page'] ?? 1);
|
|
}
|
|
|
|
if ($group = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_GP)) {
|
|
$contacts->set_group($group);
|
|
}
|
|
|
|
if (!$init_env) {
|
|
return $contacts;
|
|
}
|
|
|
|
$rcmail->output->set_env('readonly', $contacts->readonly);
|
|
$rcmail->output->set_env('source', (string) $source);
|
|
$rcmail->output->set_env('group', $group);
|
|
|
|
// reduce/extend $CONTACT_COLTYPES with specification from the current $CONTACT object
|
|
// @phpstan-ignore-next-line
|
|
if (is_array($contacts->coltypes)) {
|
|
// remove cols not listed by the backend class
|
|
$contact_cols = isset($contacts->coltypes[0]) ? array_flip($contacts->coltypes) : $contacts->coltypes;
|
|
self::$CONTACT_COLTYPES = array_intersect_key(self::$CONTACT_COLTYPES, $contact_cols);
|
|
|
|
// add associative coltypes definition
|
|
if (empty($contacts->coltypes[0])) {
|
|
foreach ($contacts->coltypes as $col => $colprop) {
|
|
if (!empty($colprop['childs'])) {
|
|
foreach ($colprop['childs'] as $childcol => $childprop) {
|
|
$colprop['childs'][$childcol] = array_merge((array) self::$CONTACT_COLTYPES[$col]['childs'][$childcol], $childprop);
|
|
}
|
|
}
|
|
|
|
if (isset(self::$CONTACT_COLTYPES[$col])) {
|
|
self::$CONTACT_COLTYPES[$col] = array_merge(self::$CONTACT_COLTYPES[$col], $colprop);
|
|
} else {
|
|
self::$CONTACT_COLTYPES[$col] = $colprop;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$rcmail->output->set_env('photocol', !empty(self::$CONTACT_COLTYPES['photo']));
|
|
|
|
return $contacts;
|
|
}
|
|
|
|
public static function set_sourcename($abook)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// get address book name (for display)
|
|
if ($abook && !empty($_SESSION['addressbooks_count']) && $_SESSION['addressbooks_count'] > 1) {
|
|
$name = $abook->get_name();
|
|
if (!$name) {
|
|
$name = $rcmail->gettext('personaladrbook');
|
|
}
|
|
|
|
$rcmail->output->set_env('sourcename', html_entity_decode($name, \ENT_COMPAT, 'UTF-8'));
|
|
}
|
|
}
|
|
|
|
public static function directory_list($attrib)
|
|
{
|
|
if (empty($attrib['id'])) {
|
|
$attrib['id'] = 'rcmdirectorylist';
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$out = '';
|
|
$jsdata = [];
|
|
|
|
$line_templ = html::tag('li',
|
|
['id' => 'rcmli%s', 'class' => '%s', 'noclose' => true],
|
|
html::a(
|
|
[
|
|
'href' => '%s',
|
|
'rel' => '%s',
|
|
'onclick' => 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('list','%s',this)",
|
|
],
|
|
'%s'
|
|
)
|
|
);
|
|
|
|
$sources = (array) $rcmail->output->get_env('address_sources');
|
|
reset($sources);
|
|
|
|
// currently selected source
|
|
$current = rcube_utils::get_input_string('_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 (!empty($source['readonly'])) {
|
|
$class_name .= ' readonly';
|
|
}
|
|
if (!empty($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(['_source' => $id])),
|
|
$source['id'],
|
|
$js_id,
|
|
$name
|
|
);
|
|
|
|
$groupdata = ['out' => $out, 'jsdata' => $jsdata, 'source' => $id];
|
|
if (!empty($source['groups'])) {
|
|
$groupdata = self::contact_groups($groupdata);
|
|
}
|
|
$jsdata = $groupdata['jsdata'];
|
|
$out = $groupdata['out'];
|
|
$out .= '</li>';
|
|
}
|
|
|
|
$rcmail->output->set_env('contactgroups', $jsdata);
|
|
$rcmail->output->set_env('collapsed_abooks', (string) $rcmail->config->get('collapsed_abooks', ''));
|
|
$rcmail->output->add_gui_object('folderlist', $attrib['id']);
|
|
$rcmail->output->include_script('treelist.js');
|
|
|
|
// add some labels to client
|
|
$rcmail->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);
|
|
}
|
|
|
|
public static function savedsearch_list($attrib)
|
|
{
|
|
if (empty($attrib['id'])) {
|
|
$attrib['id'] = 'rcmsavedsearchlist';
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$out = '';
|
|
$line_templ = html::tag('li',
|
|
['id' => 'rcmli%s', 'class' => '%s'],
|
|
html::a([
|
|
'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 = ['contactsearch'];
|
|
if (!empty($source['class_name'])) {
|
|
$classes[] = $source['class_name'];
|
|
}
|
|
|
|
$out .= sprintf($line_templ,
|
|
rcube_utils::html_identifier('S' . $id, true),
|
|
implode(' ', $classes),
|
|
$id,
|
|
$js_id,
|
|
rcube::Q($source['name'] ?: $id)
|
|
);
|
|
}
|
|
|
|
$rcmail->output->add_gui_object('savedsearchlist', $attrib['id']);
|
|
|
|
return html::tag('ul', $attrib, $out, html::$common_attrib);
|
|
}
|
|
|
|
public static function contact_groups($args)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$groups = $rcmail->get_address_book($args['source'])->list_groups();
|
|
$groups_html = '';
|
|
|
|
if (!empty($groups)) {
|
|
$line_templ = html::tag('li',
|
|
['id' => 'rcmli%s', 'class' => 'contactgroup'],
|
|
html::a([
|
|
'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 <ul>
|
|
$is_collapsed = str_contains($rcmail->config->get('collapsed_abooks', ''), '&' . rawurlencode($args['source']) . '&');
|
|
$args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' ');
|
|
|
|
foreach ($groups as $group) {
|
|
$groups_html .= sprintf($line_templ,
|
|
rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true),
|
|
$args['source'],
|
|
$group['ID'],
|
|
$args['source'],
|
|
$group['ID'],
|
|
rcube::Q($group['name'])
|
|
);
|
|
|
|
$args['jsdata']['G' . $args['source'] . $group['ID']] = [
|
|
'source' => $args['source'],
|
|
'id' => $group['ID'],
|
|
'name' => $group['name'],
|
|
'type' => 'group',
|
|
];
|
|
}
|
|
}
|
|
|
|
$style = !empty($is_collapsed) || empty($groups) ? 'display:none;' : null;
|
|
|
|
$args['out'] .= html::tag('ul', ['class' => 'groups', 'style' => $style], $groups_html);
|
|
|
|
return $args;
|
|
}
|
|
|
|
// return the contacts list as HTML table
|
|
public static function contacts_list($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// define list of cols to be displayed
|
|
$a_show_cols = ['name', 'action'];
|
|
|
|
// add id to message list table if not specified
|
|
if (empty($attrib['id'])) {
|
|
$attrib['id'] = 'rcmAddressList';
|
|
}
|
|
|
|
// create XHTML table
|
|
$out = self::table_output($attrib, [], $a_show_cols, self::$CONTACTS->primary_key);
|
|
|
|
// set client env
|
|
$rcmail->output->add_gui_object('contactslist', $attrib['id']);
|
|
$rcmail->output->set_env('current_page', (int) self::$CONTACTS->list_page);
|
|
$rcmail->output->include_script('list.js');
|
|
|
|
// add some labels to client
|
|
$rcmail->output->add_label('deletecontactconfirm', 'copyingcontact', 'movingcontact', 'contactdeleting');
|
|
|
|
return $out;
|
|
}
|
|
|
|
public static function js_contacts_list($result, $prefix = '')
|
|
{
|
|
if (empty($result) || $result->count == 0) {
|
|
return;
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// define list of cols to be displayed
|
|
$a_show_cols = ['name', 'action'];
|
|
|
|
foreach ($result as $row) {
|
|
$emails = rcube_addressbook::get_col_values('email', $row, true);
|
|
$row['CID'] = $row['ID'];
|
|
$row['email'] = array_first($emails);
|
|
$source_id = $rcmail->output->get_env('source');
|
|
$a_row_cols = [];
|
|
$type = !empty($row['_type']) ? $row['_type'] : 'person';
|
|
$classes = [$type];
|
|
|
|
// build contact ID with source ID
|
|
if (isset($row['sourceid'])) {
|
|
$row['ID'] = $row['ID'] . '-' . $row['sourceid'];
|
|
$source_id = $row['sourceid'];
|
|
}
|
|
|
|
// format each col
|
|
foreach ($a_show_cols as $col) {
|
|
$val = null;
|
|
switch ($col) {
|
|
case 'name':
|
|
$val = rcube::Q(rcube_addressbook::compose_list_name($row));
|
|
break;
|
|
case 'action':
|
|
if ($type == 'group') {
|
|
$val = html::a([
|
|
'href' => '#list',
|
|
'rel' => $row['ID'],
|
|
'title' => $rcmail->gettext('listgroup'),
|
|
'onclick' => sprintf(
|
|
"return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)",
|
|
rcmail_output::JS_OBJECT_NAME,
|
|
$source_id,
|
|
$row['CID']
|
|
),
|
|
'class' => 'pushgroup',
|
|
'data-action-link' => true,
|
|
],
|
|
'»'
|
|
);
|
|
} else {
|
|
$val = null;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
$val = rcube::Q($row[$col]);
|
|
break;
|
|
}
|
|
|
|
if ($val !== null) {
|
|
$a_row_cols[$col] = $val;
|
|
}
|
|
}
|
|
|
|
if (!empty($row['readonly'])) {
|
|
$classes[] = 'readonly';
|
|
}
|
|
|
|
$rcmail->output->command($prefix . 'add_contact_row', $row['ID'], $a_row_cols, implode(' ', $classes),
|
|
array_intersect_key($row, ['ID' => 1, 'readonly' => 1, '_type' => 1, 'email' => 1, 'name' => 1])
|
|
);
|
|
}
|
|
}
|
|
|
|
public static function contacts_list_title($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$attrib += ['label' => 'contacts', 'id' => 'rcmabooklisttitle', 'tag' => 'span'];
|
|
unset($attrib['name']);
|
|
|
|
$rcmail->output->add_gui_object('addresslist_title', $attrib['id']);
|
|
$rcmail->output->add_label('contacts', 'uponelevel');
|
|
|
|
return html::tag($attrib['tag'], $attrib, $rcmail->gettext($attrib['label']), html::$common_attrib);
|
|
}
|
|
|
|
public static function rowcount_display($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
if (empty($attrib['id'])) {
|
|
$attrib['id'] = 'rcmcountdisplay';
|
|
}
|
|
|
|
$rcmail->output->add_gui_object('countdisplay', $attrib['id']);
|
|
|
|
if (!empty($attrib['label'])) {
|
|
$_SESSION['contactcountdisplay'] = $attrib['label'];
|
|
}
|
|
|
|
return html::span($attrib, $rcmail->gettext('loading'));
|
|
}
|
|
|
|
public static function get_rowcount_text($result = null)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// read nr of contacts
|
|
if (empty($result) && !empty(self::$CONTACTS)) {
|
|
$result = self::$CONTACTS->get_result();
|
|
}
|
|
|
|
if (empty($result) || $result->count == 0) {
|
|
return $rcmail->gettext('nocontactsfound');
|
|
}
|
|
|
|
$page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50));
|
|
|
|
return $rcmail->gettext([
|
|
'name' => !empty($_SESSION['contactcountdisplay']) ? $_SESSION['contactcountdisplay'] : 'contactsfromto',
|
|
'vars' => [
|
|
'from' => $result->first + 1,
|
|
'to' => min($result->count, $result->first + $page_size),
|
|
'count' => $result->count,
|
|
],
|
|
]);
|
|
}
|
|
|
|
public static function get_type_label($type)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$label = 'type' . $type;
|
|
|
|
if ($rcmail->text_exists($label, '*', $domain)) {
|
|
return $rcmail->gettext($label, $domain);
|
|
}
|
|
|
|
if (
|
|
preg_match('/\w+(\d+)$/', $label, $m)
|
|
&& ($label = preg_replace('/(\d+)$/', '', $label))
|
|
&& $rcmail->text_exists($label, '*', $domain)
|
|
) {
|
|
return $rcmail->gettext($label, $domain) . ' ' . $m[1];
|
|
}
|
|
|
|
return ucfirst($type);
|
|
}
|
|
|
|
public static function contact_form($form, $record, $attrib = null)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// group fields
|
|
$head_fields = [
|
|
'source' => ['source'],
|
|
'names' => ['prefix', 'firstname', 'middlename', 'surname', 'suffix'],
|
|
'displayname' => ['name'],
|
|
'nickname' => ['nickname'],
|
|
'organization' => ['organization'],
|
|
'department' => ['department'],
|
|
'jobtitle' => ['jobtitle'],
|
|
];
|
|
|
|
// Allow plugins to modify contact form content
|
|
$plugin = $rcmail->plugins->exec_hook('contact_form', [
|
|
'form' => $form,
|
|
'record' => $record,
|
|
'head_fields' => $head_fields,
|
|
]);
|
|
|
|
$form = $plugin['form'];
|
|
$record = $plugin['record'];
|
|
$head_fields = $plugin['head_fields'];
|
|
$edit_mode = $rcmail->action != 'show' && $rcmail->action != 'print';
|
|
$compact = self::get_bool_attr($attrib, 'compact-form');
|
|
$use_labels = self::get_bool_attr($attrib, 'use-labels');
|
|
$with_source = self::get_bool_attr($attrib, 'with-source');
|
|
$out = '';
|
|
|
|
if (!empty($attrib['deleteicon'])) {
|
|
$del_button = html::img([
|
|
'src' => $rcmail->output->asset_url($attrib['deleteicon'], true),
|
|
'alt' => $rcmail->gettext('delete'),
|
|
]);
|
|
} else {
|
|
$del_button = html::span('inner', $rcmail->gettext('delete'));
|
|
}
|
|
|
|
unset($attrib['deleteicon']);
|
|
|
|
// get default coltypes
|
|
$coltypes = self::$CONTACT_COLTYPES;
|
|
$coltype_labels = [];
|
|
$business_mode = $rcmail->config->get('contact_form_mode') === 'business';
|
|
|
|
foreach ($coltypes as $col => $prop) {
|
|
if (!empty($prop['subtypes'])) {
|
|
// re-order subtypes, so 'work' is before 'home'
|
|
if ($business_mode) {
|
|
$work_opts = array_filter($prop['subtypes'], static function ($var) {
|
|
return str_contains($var, 'work');
|
|
});
|
|
if (!empty($work_opts)) {
|
|
$coltypes[$col]['subtypes'] = $prop['subtypes'] = array_merge(
|
|
$work_opts,
|
|
array_diff($prop['subtypes'], $work_opts)
|
|
);
|
|
}
|
|
}
|
|
|
|
$subtype_names = array_map('rcmail_action_contacts_index::get_type_label', $prop['subtypes']);
|
|
$select_subtype = new html_select([
|
|
'name' => "_subtype_{$col}[]",
|
|
'class' => 'contactselectsubtype custom-select',
|
|
'title' => $prop['label'] . ' ' . $rcmail->gettext('type'),
|
|
]);
|
|
$select_subtype->add($subtype_names, $prop['subtypes']);
|
|
|
|
$coltypes[$col]['subtypes_select'] = $select_subtype->show();
|
|
}
|
|
|
|
if (!empty($prop['childs'])) {
|
|
foreach ($prop['childs'] as $childcol => $cp) {
|
|
$coltype_labels[$childcol] = ['label' => $cp['label']];
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($form as $section => $fieldset) {
|
|
// skip empty sections
|
|
if (empty($fieldset['content'])) {
|
|
continue;
|
|
}
|
|
|
|
$select_add = new html_select([
|
|
'class' => 'addfieldmenu custom-select',
|
|
'rel' => $section,
|
|
'data-compact' => $compact ? 'true' : null,
|
|
]);
|
|
|
|
$select_add->add($rcmail->gettext('addfield'), '');
|
|
$select_add_count = 0;
|
|
|
|
// render head section with name fields (not a regular list of rows)
|
|
if ($section == 'head') {
|
|
$content = '';
|
|
|
|
// unset display name if it is composed from name parts
|
|
$dname = rcube_addressbook::compose_display_name(['name' => ''] + (array) $record);
|
|
if (isset($record['name']) && $record['name'] == $dname) {
|
|
unset($record['name']);
|
|
}
|
|
|
|
foreach ($head_fields as $blockname => $colnames) {
|
|
$fields = '';
|
|
$block_attr = ['class' => $blockname . (count($colnames) == 1 ? ' row' : '')];
|
|
|
|
foreach ($colnames as $col) {
|
|
if ($col == 'source') {
|
|
if (!$with_source || !($source = $rcmail->output->get_env('sourcename'))) {
|
|
continue;
|
|
}
|
|
|
|
if (!$edit_mode) {
|
|
$record['source'] = $rcmail->gettext('addressbook') . ': ' . $source;
|
|
} elseif ($rcmail->action == 'add') {
|
|
$record['source'] = $source;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
// skip cols unknown to the backend
|
|
elseif (empty($coltypes[$col])) {
|
|
continue;
|
|
}
|
|
|
|
// skip cols not listed in the form definition
|
|
if (is_array($fieldset['content']) && !in_array($col, array_keys($fieldset['content']))) {
|
|
continue;
|
|
}
|
|
|
|
// only string values are expected here
|
|
if (isset($record[$col]) && is_array($record[$col])) {
|
|
$record[$col] = implode(' ', $record[$col]);
|
|
}
|
|
|
|
if (!$edit_mode) {
|
|
if (!empty($record[$col])) {
|
|
$fields .= html::span('namefield ' . $col, rcube::Q($record[$col])) . ' ';
|
|
}
|
|
} else {
|
|
$visible = true;
|
|
$colprop = [];
|
|
|
|
if (!empty($fieldset['content'][$col])) {
|
|
$colprop += (array) $fieldset['content'][$col];
|
|
}
|
|
|
|
if (!empty($coltypes[$col])) {
|
|
$colprop += (array) $coltypes[$col];
|
|
}
|
|
|
|
if (empty($colprop['id'])) {
|
|
$colprop['id'] = 'ff_' . $col;
|
|
}
|
|
|
|
if (empty($record[$col]) && empty($colprop['visible'])) {
|
|
$visible = false;
|
|
$colprop['style'] = $use_labels ? null : 'display:none';
|
|
$select_add->add($colprop['label'], $col);
|
|
}
|
|
|
|
if ($col == 'source') {
|
|
$input = self::source_selector(['id' => $colprop['id']]);
|
|
} else {
|
|
$val = $record[$col] ?? null;
|
|
$input = rcube_output::get_edit_field($col, $val, $colprop);
|
|
}
|
|
|
|
if ($use_labels) {
|
|
$_content = html::label($colprop['id'], rcube::Q($colprop['label'])) . html::div(null, $input);
|
|
if (count($colnames) > 1) {
|
|
$fields .= html::div(['class' => 'row', 'style' => $visible ? null : 'display:none'], $_content);
|
|
} else {
|
|
$fields .= $_content;
|
|
$block_attr['style'] = $visible ? null : 'display:none';
|
|
}
|
|
} else {
|
|
$fields .= $input;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($fields) {
|
|
$content .= html::div($block_attr, $fields);
|
|
}
|
|
}
|
|
|
|
if ($edit_mode) {
|
|
$content .= html::p('addfield', $select_add->show(null));
|
|
}
|
|
|
|
$legend = !empty($fieldset['name']) ? html::tag('legend', null, rcube::Q($fieldset['name'])) : '';
|
|
$out .= html::tag('fieldset', $attrib, $legend . $content, html::$common_attrib) . "\n";
|
|
continue;
|
|
}
|
|
|
|
$content = '';
|
|
if (is_array($fieldset['content'])) {
|
|
foreach ($fieldset['content'] as $col => $colprop) {
|
|
// remove subtype part of col name
|
|
$tokens = explode(':', $col);
|
|
$field = $tokens[0];
|
|
|
|
if (empty($tokens[1])) {
|
|
$subtype = $business_mode ? 'work' : 'home';
|
|
} else {
|
|
$subtype = $tokens[1];
|
|
}
|
|
|
|
// skip cols unknown to the backend
|
|
if (empty($coltypes[$field]) && empty($colprop['value'])) {
|
|
continue;
|
|
}
|
|
|
|
// merge colprop with global coltype configuration
|
|
if (!empty($coltypes[$field])) {
|
|
$colprop += $coltypes[$field];
|
|
}
|
|
|
|
if (!isset($colprop['type'])) {
|
|
$colprop['type'] = 'text';
|
|
}
|
|
|
|
$label = $colprop['label'] ?? $rcmail->gettext($col);
|
|
|
|
// prepare subtype selector in edit mode
|
|
if ($edit_mode && isset($colprop['subtypes']) && is_array($colprop['subtypes'])) {
|
|
$subtype_names = array_map('rcmail_action_contacts_index::get_type_label', $colprop['subtypes']);
|
|
$select_subtype = new html_select([
|
|
'name' => "_subtype_{$col}[]",
|
|
'class' => 'contactselectsubtype custom-select',
|
|
'title' => $colprop['label'] . ' ' . $rcmail->gettext('type'),
|
|
]);
|
|
$select_subtype->add($subtype_names, $colprop['subtypes']);
|
|
} else {
|
|
$select_subtype = null;
|
|
}
|
|
|
|
$rows = '';
|
|
|
|
[$values, $subtypes] = self::contact_field_values($record, "{$field}:{$subtype}", $colprop);
|
|
|
|
foreach ($values as $i => $val) {
|
|
if (!empty($subtypes[$i])) {
|
|
$subtype = $subtypes[$i];
|
|
}
|
|
|
|
$fc = intval($coltypes[$field]['count'] ?? 0);
|
|
$colprop['id'] = 'ff_' . $col . $fc;
|
|
$row_class = 'row';
|
|
|
|
// render composite field
|
|
if ($colprop['type'] == 'composite') {
|
|
$row_class .= ' composite';
|
|
$composite = [];
|
|
$template = $rcmail->config->get($col . '_template', '{' . implode('} {', array_keys($colprop['childs'])) . '}');
|
|
$j = 0;
|
|
|
|
foreach ($colprop['childs'] as $childcol => $cp) {
|
|
if (!empty($val) && is_array($val)) {
|
|
if (!empty($val[$childcol])) {
|
|
$childvalue = $val[$childcol];
|
|
} else {
|
|
$childvalue = $val[$j] ?? null;
|
|
}
|
|
} else {
|
|
$childvalue = '';
|
|
}
|
|
|
|
if ($edit_mode) {
|
|
if (!empty($colprop['subtypes']) || $colprop['limit'] != 1) {
|
|
$cp['array'] = true;
|
|
}
|
|
|
|
$cp_type = $cp['type'] ?? null;
|
|
$composite['{' . $childcol . '}'] = rcube_output::get_edit_field($childcol, $childvalue, $cp, $cp_type) . ' ';
|
|
} else {
|
|
if (!empty($cp['render_func'])) {
|
|
$childval = call_user_func($cp['render_func'], $childvalue, $childcol);
|
|
} else {
|
|
$childval = rcube::Q($childvalue);
|
|
}
|
|
|
|
$composite['{' . $childcol . '}'] = html::span('data ' . $childcol, $childval) . ' ';
|
|
}
|
|
|
|
$j++;
|
|
}
|
|
|
|
$coltypes[$field] += (array) $colprop;
|
|
|
|
if (isset($coltypes[$field]['count'])) {
|
|
$coltypes[$field]['count']++;
|
|
} else {
|
|
$coltypes[$field]['count'] = 1;
|
|
}
|
|
|
|
$val = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
|
|
|
|
if ($compact) {
|
|
$val = html::div('content', str_replace('<br/>', '', $val));
|
|
}
|
|
} elseif ($edit_mode) {
|
|
// call callback to render/format value
|
|
if (!empty($colprop['render_func'])) {
|
|
$val = call_user_func($colprop['render_func'], $val, $col);
|
|
}
|
|
|
|
$coltypes[$field] = (array) $colprop + $coltypes[$field];
|
|
|
|
if (!empty($colprop['subtypes']) || $colprop['limit'] != 1) {
|
|
$colprop['array'] = true;
|
|
}
|
|
|
|
// load jquery UI datepicker for date fields
|
|
if (isset($colprop['type']) && $colprop['type'] == 'date') {
|
|
$colprop['class'] = (!empty($colprop['class']) ? $colprop['class'] . ' ' : '') . 'datepicker';
|
|
if (empty($colprop['render_func'])) {
|
|
$val = self::format_date_col($val);
|
|
}
|
|
}
|
|
|
|
$val = rcube_output::get_edit_field($col, $val, $colprop, $colprop['type']);
|
|
|
|
if (empty($coltypes[$field]['count'])) {
|
|
$coltypes[$field]['count'] = 1;
|
|
} else {
|
|
$coltypes[$field]['count']++;
|
|
}
|
|
} elseif (!empty($colprop['render_func'])) {
|
|
$val = call_user_func($colprop['render_func'], $val, $col);
|
|
} elseif (isset($colprop['options']) && isset($colprop['options'][$val])) {
|
|
$val = $colprop['options'][$val];
|
|
} else {
|
|
$val = rcube::Q($val);
|
|
}
|
|
|
|
// use subtype as label
|
|
if (!empty($colprop['subtypes'])) {
|
|
$label = self::get_type_label($subtype);
|
|
}
|
|
|
|
$_del_btn = html::a([
|
|
'href' => '#del',
|
|
'class' => 'contactfieldbutton deletebutton',
|
|
'title' => $rcmail->gettext('delete'),
|
|
'rel' => $col,
|
|
],
|
|
$del_button
|
|
);
|
|
|
|
// add delete button/link
|
|
if (!$compact && $edit_mode
|
|
&& (empty($colprop['visible']) || empty($colprop['limit']) || $colprop['limit'] > 1)
|
|
) {
|
|
$val .= $_del_btn;
|
|
}
|
|
|
|
// display row with label
|
|
if ($label) {
|
|
if ($rcmail->action == 'print') {
|
|
$_label = rcube::Q($colprop['label'] . ($label != $colprop['label'] ? ' (' . $label . ')' : ''));
|
|
if (!$compact) {
|
|
$_label = html::div('contactfieldlabel label', $_label);
|
|
}
|
|
} elseif ($select_subtype) {
|
|
$_label = $select_subtype->show($subtype);
|
|
if (!$compact) {
|
|
$_label = html::div('contactfieldlabel label', $_label);
|
|
}
|
|
} else {
|
|
$_label = html::label(['class' => 'contactfieldlabel label', 'for' => $colprop['id']], rcube::Q($label));
|
|
}
|
|
|
|
if (!$compact) {
|
|
$val = html::div('contactfieldcontent ' . $colprop['type'], $val);
|
|
} else {
|
|
$val .= $_del_btn;
|
|
}
|
|
|
|
$rows .= html::div($row_class, $_label . $val);
|
|
}
|
|
// row without label
|
|
else {
|
|
$rows .= html::div($row_class, $compact ? $val : html::div('contactfield', $val));
|
|
}
|
|
}
|
|
|
|
// add option to the add-field menu
|
|
if (empty($colprop['limit']) || empty($coltypes[$field]['count']) || $coltypes[$field]['count'] < $colprop['limit']) {
|
|
$select_add->add($colprop['label'], $col);
|
|
$select_add_count++;
|
|
}
|
|
|
|
// wrap rows in fieldgroup container
|
|
if ($rows) {
|
|
$c_class = 'contactfieldgroup '
|
|
. (!empty($colprop['subtypes']) ? 'contactfieldgroupmulti ' : '')
|
|
. 'contactcontroller' . $col;
|
|
$with_label = !empty($colprop['subtypes']) && $rcmail->action != 'print';
|
|
$content .= html::tag(
|
|
'fieldset',
|
|
['class' => $c_class],
|
|
($with_label ? html::tag('legend', null, rcube::Q($colprop['label'])) : ' ') . $rows
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!$content && (!$edit_mode || !$select_add_count)) {
|
|
continue;
|
|
}
|
|
|
|
// also render add-field selector
|
|
if ($edit_mode) {
|
|
$content .= html::p('addfield', $select_add->show(null, ['style' => $select_add_count ? null : 'display:none']));
|
|
}
|
|
|
|
$content = html::div(['id' => 'contactsection' . $section], $content);
|
|
} else {
|
|
$content = $fieldset['content'];
|
|
}
|
|
|
|
if ($content) {
|
|
$fattribs = !empty($attrib['fieldset-class']) ? ['class' => $attrib['fieldset-class']] : null;
|
|
$fcontent = html::tag('legend', null, rcube::Q($fieldset['name'])) . $content;
|
|
$out .= html::tag('fieldset', $fattribs, $fcontent) . "\n";
|
|
}
|
|
}
|
|
|
|
if ($edit_mode) {
|
|
$rcmail->output->set_env('coltypes', $coltypes + $coltype_labels);
|
|
$rcmail->output->set_env('delbutton', $del_button);
|
|
$rcmail->output->add_label('delete');
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
public static function contact_field_values($record, $field_name, $colprop)
|
|
{
|
|
[$field, $subtype] = explode(':', $field_name);
|
|
|
|
$subtypes = [];
|
|
$values = [];
|
|
|
|
if (!empty($colprop['value'])) {
|
|
$values = (array) $colprop['value'];
|
|
} elseif (!empty($colprop['subtypes'])) {
|
|
// iterate over possible subtypes and collect values with their subtype
|
|
$c_values = rcube_addressbook::get_col_values($field, $record);
|
|
|
|
foreach ($colprop['subtypes'] as $st) {
|
|
if (isset($c_values[$st])) {
|
|
foreach ((array) $c_values[$st] as $value) {
|
|
$i = count($values);
|
|
$subtypes[$i] = $st;
|
|
$values[$i] = $value;
|
|
}
|
|
|
|
$c_values[$st] = null;
|
|
}
|
|
}
|
|
|
|
// TODO: add $st to $select_subtype if missing ?
|
|
foreach ($c_values as $st => $vals) {
|
|
foreach ((array) $vals as $value) {
|
|
$i = count($values);
|
|
$subtypes[$i] = $st;
|
|
$values[$i] = $value;
|
|
}
|
|
}
|
|
} elseif (isset($record[$field_name])) {
|
|
$values = $record[$field_name];
|
|
} elseif (isset($record[$field])) {
|
|
$values = $record[$field];
|
|
}
|
|
|
|
// hack: create empty values array to force this field to be displayed
|
|
if (empty($values) && !empty($colprop['visible'])) {
|
|
$values = [''];
|
|
}
|
|
|
|
if (!is_array($values)) {
|
|
// $values can be an object, don't use (array)$values syntax
|
|
$values = !empty($values) ? [$values] : [];
|
|
}
|
|
|
|
return [$values, $subtypes];
|
|
}
|
|
|
|
public static function contact_photo($attrib)
|
|
{
|
|
if ($result = self::$CONTACTS->get_result()) {
|
|
$record = $result->first();
|
|
} else {
|
|
$record = ['photo' => null, '_type' => 'contact'];
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
if (!empty($record['_type']) && $record['_type'] == 'group' && !empty($attrib['placeholdergroup'])) {
|
|
$photo_img = $rcmail->output->abs_url($attrib['placeholdergroup'], true);
|
|
$photo_img = $rcmail->output->asset_url($photo_img);
|
|
} elseif (!empty($attrib['placeholder'])) {
|
|
$photo_img = $rcmail->output->abs_url($attrib['placeholder'], true);
|
|
$photo_img = $rcmail->output->asset_url($photo_img);
|
|
} else {
|
|
$photo_img = 'data:image/gif;base64,' . rcmail_output::BLANK_GIF;
|
|
}
|
|
|
|
$rcmail->output->set_env('photo_placeholder', $photo_img);
|
|
|
|
unset($attrib['placeholder']);
|
|
|
|
$plugin = $rcmail->plugins->exec_hook('contact_photo', [
|
|
'record' => $record,
|
|
'data' => $record['photo'] ?? null,
|
|
'attrib' => $attrib,
|
|
]);
|
|
|
|
// check if we have photo data from contact form
|
|
if (!empty(self::$contact)) {
|
|
if (!empty(self::$contact['photo'])) {
|
|
if (self::$contact['photo'] == '-del-') {
|
|
$record['photo'] = '';
|
|
} elseif (preg_match('/^[a-z0-9]+$/i', self::$contact['photo']) && $rcmail->get_uploaded_file(self::$contact['photo'])) {
|
|
$record['photo'] = $file_id = self::$contact['photo'];
|
|
}
|
|
}
|
|
}
|
|
|
|
$ff_value = '';
|
|
|
|
if (!empty($plugin['url'])) {
|
|
$photo_img = $plugin['url'];
|
|
} elseif (!empty($record['photo']) && preg_match('!^https?://!i', $record['photo'])) {
|
|
$photo_img = $record['photo'];
|
|
} elseif (!empty($record['photo'])) {
|
|
$url = ['_action' => 'photo', '_cid' => $record['ID'], '_source' => self::$SOURCE_ID];
|
|
if (!empty($file_id)) {
|
|
$url['_photo'] = $ff_value = $file_id;
|
|
}
|
|
$photo_img = $rcmail->url($url);
|
|
} else {
|
|
$ff_value = '-del-'; // will disable delete-photo action
|
|
}
|
|
|
|
$content = html::div($attrib, html::img([
|
|
'src' => $photo_img,
|
|
'onerror' => 'this.onerror = null; this.src = rcmail.env.photo_placeholder;',
|
|
]));
|
|
|
|
if (!empty(self::$CONTACT_COLTYPES['photo']) && ($rcmail->action == 'edit' || $rcmail->action == 'add')) {
|
|
$rcmail->output->add_gui_object('contactphoto', $attrib['id']);
|
|
$hidden = new html_hiddenfield(['name' => '_photo', 'id' => 'ff_photo', 'value' => $ff_value]);
|
|
$content .= $hidden->show();
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
public static function format_date_col($val)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
return $rcmail->format_date($val, $rcmail->config->get('date_format', 'Y-m-d'), false);
|
|
}
|
|
|
|
/**
|
|
* Updates saved search after data changed
|
|
*/
|
|
public static function search_update($return = false)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
if (empty($_REQUEST['_search'])) {
|
|
return false;
|
|
}
|
|
|
|
$search_request = $_REQUEST['_search'];
|
|
|
|
if (!isset($_SESSION['contact_search'][$search_request])) {
|
|
return false;
|
|
}
|
|
|
|
$search = (array) $_SESSION['contact_search'][$search_request];
|
|
$sort_col = $rcmail->config->get('addressbook_sort_col', 'name');
|
|
$afields = $return ? $rcmail->config->get('contactlist_fields') : ['name', 'email'];
|
|
$records = [];
|
|
|
|
foreach ($search as $s => $set) {
|
|
$source = $rcmail->get_address_book($s);
|
|
|
|
// reset page
|
|
$source->set_page(1);
|
|
$source->set_pagesize(9999);
|
|
$source->set_search_set($set);
|
|
|
|
// get records
|
|
$result = $source->list_records($afields);
|
|
|
|
if (!$result->count) {
|
|
unset($search[$s]);
|
|
continue;
|
|
}
|
|
|
|
if ($return) {
|
|
foreach ($result as $row) {
|
|
$row['sourceid'] = $s;
|
|
$key = rcube_addressbook::compose_contact_key($row, $sort_col);
|
|
$records[$key] = $row;
|
|
}
|
|
|
|
unset($result);
|
|
}
|
|
|
|
$search[$s] = $source->get_search_set();
|
|
}
|
|
|
|
$_SESSION['contact_search'][$search_request] = $search;
|
|
|
|
return $records;
|
|
}
|
|
|
|
/**
|
|
* Returns contact ID(s) and source(s) from GET/POST data
|
|
*
|
|
* @param string $filter Return contact identifier for this specific source
|
|
* @param int $request_type Type of the input var (rcube_utils::INPUT_*)
|
|
*
|
|
* @return array List of contact IDs per-source
|
|
*/
|
|
public static function get_cids($filter = null, $request_type = rcube_utils::INPUT_GPC)
|
|
{
|
|
// contact ID (or comma-separated list of IDs) is provided in two
|
|
// forms. If _source is an empty string then the ID is a string
|
|
// containing contact ID and source name in form: <ID>-<SOURCE>
|
|
|
|
$cid = rcube_utils::get_input_value('_cid', $request_type);
|
|
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
|
|
|
|
if (is_array($cid)) {
|
|
return $cid;
|
|
}
|
|
|
|
if (!is_string($cid) || !preg_match('/^[a-zA-Z0-9\+\/=_-]+(,[a-zA-Z0-9\+\/=_-]+)*$/', $cid)) {
|
|
return [];
|
|
}
|
|
|
|
$cid = explode(',', $cid);
|
|
$got_source = strlen($source);
|
|
$result = [];
|
|
|
|
// create per-source contact IDs array
|
|
foreach ($cid as $id) {
|
|
// extract source ID from contact ID (it's there in search mode)
|
|
// see #1488959 and #1488862 for reference
|
|
if (!$got_source) {
|
|
if ($sep = strrpos($id, '-')) {
|
|
$contact_id = substr($id, 0, $sep);
|
|
$source_id = (string) substr($id, $sep + 1);
|
|
if (strlen($source_id)) {
|
|
$result[$source_id][] = $contact_id;
|
|
}
|
|
}
|
|
} else {
|
|
if (substr($id, -($got_source + 1)) === "-{$source}") {
|
|
$id = substr($id, 0, -($got_source + 1));
|
|
}
|
|
$result[$source][] = $id;
|
|
}
|
|
}
|
|
|
|
return $filter !== null ? $result[$filter] : $result;
|
|
}
|
|
|
|
/**
|
|
* Returns HTML code for an addressbook selector
|
|
*
|
|
* @param array $attrib Template object attributes
|
|
*
|
|
* @return string HTML code of a <select> element, or <span> if there's only one writeable source
|
|
*/
|
|
public static function source_selector($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$sources_list = $rcmail->get_address_sources(true, true);
|
|
|
|
if (count($sources_list) < 2) {
|
|
$source = $sources_list[self::$SOURCE_ID];
|
|
$hiddenfield = new html_hiddenfield(['name' => '_source', 'value' => self::$SOURCE_ID]);
|
|
|
|
return html::span($attrib, $source['name'] . $hiddenfield->show());
|
|
}
|
|
|
|
$attrib['name'] = '_source';
|
|
$attrib['is_escaped'] = true;
|
|
$attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . ".command('save', 'reload', this.form)";
|
|
|
|
$select = new html_select($attrib);
|
|
|
|
foreach ($sources_list as $source) {
|
|
$select->add($source['name'], $source['id']);
|
|
}
|
|
|
|
return $select->show(self::$SOURCE_ID);
|
|
}
|
|
}
|