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