source: subversion/trunk/roundcubemail/program/include/rcube_contacts.php @ 4498

Last change on this file since 4498 was 4498, checked in by thomasb, 2 years ago

Delegate contact input validation to rcube_addressbook instance; accept already localized texts in rcube_output::show_message()

  • Property svn:keywords set to Id
File size: 22.6 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_contacts.php                                    |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2006-2010, The Roundcube Dev Team                       |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   Interface to the local address book database                        |
13 |                                                                       |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16 +-----------------------------------------------------------------------+
17
18 $Id$
19
20*/
21
22
23/**
24 * Model class for the local address book database
25 *
26 * @package Addressbook
27 */
28class rcube_contacts extends rcube_addressbook
29{
30    // protected for backward compat. with some plugins
31    protected $db_name = 'contacts';
32    protected $db_groups = 'contactgroups';
33    protected $db_groupmembers = 'contactgroupmembers';
34
35    /**
36     * Store database connection.
37     *
38     * @var rcube_mdb2
39     */
40    private $db = null;
41    private $user_id = 0;
42    private $filter = null;
43    private $result = null;
44    private $search_fields;
45    private $search_string;
46    private $cache;
47    private $table_cols = array('name', 'email', 'firstname', 'surname', 'vcard');
48
49    // public properties
50    public $primary_key = 'contact_id';
51    public $readonly = false;
52    public $groups = true;
53    public $list_page = 1;
54    public $page_size = 10;
55    public $group_id = 0;
56    public $ready = false;
57    public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname',
58      'jobtitle', 'organization', 'department', 'assistant', 'manager',
59      'gender', 'maidenname', 'spouse', 'email', 'phone', 'address',
60      'birthday', 'anniversary', 'website', 'im', 'notes', 'photo');
61
62
63    /**
64     * Object constructor
65     *
66     * @param object  Instance of the rcube_db class
67     * @param integer User-ID
68     */
69    function __construct($dbconn, $user)
70    {
71        $this->db = $dbconn;
72        $this->user_id = $user;
73        $this->ready = $this->db && !$this->db->is_error();
74    }
75
76
77    /**
78     * Save a search string for future listings
79     *
80     * @param  string SQL params to use in listing method
81     */
82    function set_search_set($filter)
83    {
84        $this->filter = $filter;
85        $this->cache = null;
86    }
87
88
89    /**
90     * Getter for saved search properties
91     *
92     * @return mixed Search properties used by this class
93     */
94    function get_search_set()
95    {
96        return $this->filter;
97    }
98
99
100    /**
101     * Setter for the current group
102     * (empty, has to be re-implemented by extending class)
103     */
104    function set_group($gid)
105    {
106        $this->group_id = $gid;
107        $this->cache = null;
108    }
109
110
111    /**
112     * Reset all saved results and search parameters
113     */
114    function reset()
115    {
116        $this->result = null;
117        $this->filter = null;
118        $this->search_fields = null;
119        $this->search_string = null;
120        $this->cache = null;
121    }
122
123
124    /**
125     * List all active contact groups of this source
126     *
127     * @param string  Search string to match group name
128     * @return array  Indexed list of contact groups, each a hash array
129     */
130    function list_groups($search = null)
131    {
132        $results = array();
133
134        if (!$this->groups)
135            return $results;
136
137        $sql_filter = $search ? " AND " . $this->db->ilike('name', '%'.$search.'%') : '';
138
139        $sql_result = $this->db->query(
140            "SELECT * FROM ".get_table_name($this->db_groups).
141            " WHERE del<>1".
142            " AND user_id=?".
143            $sql_filter.
144            " ORDER BY name",
145            $this->user_id);
146
147        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
148            $sql_arr['ID'] = $sql_arr['contactgroup_id'];
149            $results[]     = $sql_arr;
150        }
151
152        return $results;
153    }
154
155
156    /**
157     * List the current set of contact records
158     *
159     * @param  array   List of cols to show, Null means all
160     * @param  int     Only return this number of records, use negative values for tail
161     * @param  boolean True to skip the count query (select only)
162     * @return array  Indexed list of contact records, each a hash array
163     */
164    function list_records($cols=null, $subset=0, $nocount=false)
165    {
166        if ($nocount || $this->list_page <= 1) {
167            // create dummy result, we don't need a count now
168            $this->result = new rcube_result_set();
169        } else {
170            // count all records
171            $this->result = $this->count();
172        }
173
174        $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
175        $length = $subset != 0 ? abs($subset) : $this->page_size;
176
177        if ($this->group_id)
178            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
179                " ON (m.contact_id = c.".$this->primary_key.")";
180
181        $sql_result = $this->db->limitquery(
182            "SELECT * FROM ".get_table_name($this->db_name)." AS c" .
183            $join .
184            " WHERE c.del<>1" .
185                " AND c.user_id=?" .
186                ($this->group_id ? " AND m.contactgroup_id=?" : "").
187                ($this->filter ? " AND (".$this->filter.")" : "") .
188            " ORDER BY c.name",
189            $start_row,
190            $length,
191            $this->user_id,
192            $this->group_id);
193
194        // determine whether we have to parse the vcard or if only db cols are requested
195        $read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
196       
197        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
198            $sql_arr['ID'] = $sql_arr[$this->primary_key];
199
200            if ($read_vcard)
201                $sql_arr = $this->convert_db_data($sql_arr);
202            else
203                $sql_arr['email'] = preg_split('/,\s*/', $sql_arr['email']);
204           
205            // make sure we have a name to display
206            if (empty($sql_arr['name']))
207                $sql_arr['name'] = $sql_arr['email'][0];
208
209            $this->result->add($sql_arr);
210        }
211
212        $cnt = count($this->result->records);
213
214        // update counter
215        if ($nocount)
216            $this->result->count = $cnt;
217        else if ($this->list_page <= 1) {
218            if ($cnt < $this->page_size && $subset == 0)
219                $this->result->count = $cnt;
220            else if (isset($this->cache['count']))
221                $this->result->count = $this->cache['count'];
222            else
223                $this->result->count = $this->_count();
224        }
225
226        return $this->result;
227    }
228
229
230    /**
231     * Search contacts
232     *
233     * @param array   List of fields to search in
234     * @param string  Search value
235     * @param boolean True for strict (=), False for partial (LIKE) matching
236     * @param boolean True if results are requested, False if count only
237     * @param boolean True to skip the count query (select only)
238     * @param array   List of fields that cannot be empty
239     * @return object rcube_result_set Contact records and 'count' value
240     */
241    function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
242    {
243        if (!is_array($fields))
244            $fields = array($fields);
245        if (!is_array($required) && !empty($required))
246            $required = array($required);
247
248        $where = $and_where = array();
249
250        foreach ($fields as $col) {
251            if ($col == 'ID' || $col == $this->primary_key) {
252                $ids     = !is_array($value) ? explode(',', $value) : $value;
253                $ids     = $this->db->array2list($ids, 'integer');
254                $where[] = 'c.' . $this->primary_key.' IN ('.$ids.')';
255            }
256            else if ($strict)
257                $where[] = $this->db->quoteIdentifier($col).' = '.$this->db->quote($value);
258            else
259                $where[] = $this->db->ilike($col, '%'.$value.'%');
260        }
261
262        foreach ($required as $col) {
263            $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote('');
264        }
265
266        if (!empty($where))
267            $where = join(' OR ', $where);
268
269        if (!empty($and_where))
270            $where = ($where ? "($where) AND " : '') . join(' AND ', $and_where);
271
272        if (!empty($where)) {
273            $this->set_search_set($where);
274            if ($select)
275                $this->list_records(null, 0, $nocount);
276            else
277                $this->result = $this->count();
278        }
279
280        return $this->result; 
281    }
282
283
284    /**
285     * Count number of available contacts in database
286     *
287     * @return rcube_result_set Result object
288     */
289    function count()
290    {
291        $count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count();
292
293        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
294    }
295
296
297    /**
298     * Count number of available contacts in database
299     *
300     * @return int Contacts count
301     */
302    private function _count()
303    {
304        if ($this->group_id)
305            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
306                " ON (m.contact_id=c.".$this->primary_key.")";
307
308        // count contacts for this user
309        $sql_result = $this->db->query(
310            "SELECT COUNT(c.contact_id) AS rows".
311            " FROM ".get_table_name($this->db_name)." AS c".
312                $join.
313            " WHERE c.del<>1".
314            " AND c.user_id=?".
315            ($this->group_id ? " AND m.contactgroup_id=?" : "").
316            ($this->filter ? " AND (".$this->filter.")" : ""),
317            $this->user_id,
318            $this->group_id
319        );
320
321        $sql_arr = $this->db->fetch_assoc($sql_result);
322
323        $this->cache['count'] = (int) $sql_arr['rows'];
324
325        return $this->cache['count'];
326    }
327
328
329    /**
330     * Return the last result set
331     *
332     * @return mixed Result array or NULL if nothing selected yet
333     */
334    function get_result()
335    {
336        return $this->result;
337    }
338
339
340    /**
341     * Get a specific contact record
342     *
343     * @param mixed record identifier(s)
344     * @return mixed Result object with all record fields or False if not found
345     */
346    function get_record($id, $assoc=false)
347    {
348        // return cached result
349        if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id)
350            return $assoc ? $first : $this->result;
351
352        $this->db->query(
353            "SELECT * FROM ".get_table_name($this->db_name).
354            " WHERE contact_id=?".
355                " AND user_id=?".
356                " AND del<>1",
357            $id,
358            $this->user_id
359        );
360
361        if ($sql_arr = $this->db->fetch_assoc()) {
362            $record = $this->convert_db_data($sql_arr);
363            $this->result = new rcube_result_set(1);
364            $this->result->add($record);
365        }
366
367        return $assoc && $record ? $record : $this->result;
368    }
369
370
371    /**
372     * Get group assignments of a specific contact record
373     *
374     * @param mixed Record identifier
375     * @return array List of assigned groups as ID=>Name pairs
376     */
377    function get_record_groups($id)
378    {
379      $results = array();
380
381      if (!$this->groups)
382          return $results;
383
384      $sql_result = $this->db->query(
385        "SELECT cgm.contactgroup_id, cg.name FROM " . get_table_name($this->db_groupmembers) . " AS cgm" .
386        " LEFT JOIN " . get_table_name($this->db_groups) . " AS cg ON (cgm.contactgroup_id = cg.contactgroup_id AND cg.del<>1)" .
387        " WHERE cgm.contact_id=?",
388        $id
389      );
390      while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
391        $results[$sql_arr['contactgroup_id']] = $sql_arr['name'];
392      }
393
394      return $results;
395    }
396
397
398    /**
399     * Check the given data before saving.
400     * If input not valid, the message to display can be fetched using get_error()
401     *
402     * @param array Assoziative array with data to save
403     * @return boolean True if input is valid, False if not.
404     */
405    public function validate($save_data)
406    {
407        // check for name input
408        $valid = parent::validate($save_data);
409
410        // require at least one e-mail address (syntax check is done later in save.inc)
411        if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) {
412            $this->set_error('warning', 'noemailwarning');
413            $valid = false;
414        }
415
416        return $valid;
417    }
418
419
420    /**
421     * Create a new contact record
422     *
423     * @param array Associative array with save data
424     * @return integer|boolean The created record ID on success, False on error
425     */
426    function insert($save_data, $check=false)
427    {
428        if (!is_array($save_data))
429            return false;
430
431        $insert_id = $existing = false;
432
433        if ($check) {
434            foreach ($save_data as $col => $values) {
435                if (strpos($col, 'email') === 0) {
436                    foreach ((array)$values as $email) {
437                        if ($existing = $this->search('email', $email, true, false))
438                            break 2;
439                    }
440                }
441            }
442        }
443
444        $save_data = $this->convert_save_data($save_data);
445        $a_insert_cols = $a_insert_values = array();
446
447        foreach ($save_data as $col => $value) {
448            $a_insert_cols[]   = $this->db->quoteIdentifier($col);
449            $a_insert_values[] = $this->db->quote($value);
450        }
451
452        if (!$existing->count && !empty($a_insert_cols)) {
453            $this->db->query(
454                "INSERT INTO ".get_table_name($this->db_name).
455                " (user_id, changed, del, ".join(', ', $a_insert_cols).")".
456                " VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
457            );
458
459            $insert_id = $this->db->insert_id($this->db_name);
460        }
461
462        // also add the newly created contact to the active group
463        if ($insert_id && $this->group_id)
464            $this->add_to_group($this->group_id, $insert_id);
465
466        $this->cache = null;
467
468        return $insert_id;
469    }
470
471
472    /**
473     * Update a specific contact record
474     *
475     * @param mixed Record identifier
476     * @param array Assoziative array with save data
477     * @return boolean True on success, False on error
478     */
479    function update($id, $save_cols)
480    {
481        $updated = false;
482        $write_sql = array();
483        $record = $this->get_record($id, true);
484        $save_cols = $this->convert_save_data($save_cols, $record);
485
486        foreach ($save_cols as $col => $value) {
487            $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
488        }
489
490        if (!empty($write_sql)) {
491            $this->db->query(
492                "UPDATE ".get_table_name($this->db_name).
493                " SET changed=".$this->db->now().", ".join(', ', $write_sql).
494                " WHERE contact_id=?".
495                    " AND user_id=?".
496                    " AND del<>1",
497                $id,
498                $this->user_id
499            );
500
501            $updated = $this->db->affected_rows();
502            $this->result = null;  // clear current result (from get_record())
503        }
504
505        return $updated;
506    }
507   
508   
509    private function convert_db_data($sql_arr)
510    {
511        $record = array();
512        $record['ID'] = $sql_arr[$this->primary_key];
513       
514        if ($sql_arr['vcard']) {
515            unset($sql_arr['email']);
516            $vcard = new rcube_vcard($sql_arr['vcard']);
517            $record += $vcard->get_assoc() + $sql_arr;
518        }
519        else {
520            $record += $sql_arr;
521            $record['email'] = preg_split('/,\s*/', $record['email']);
522        }
523       
524        return $record;
525    }
526
527
528    private function convert_save_data($save_data, $record = array())
529    {
530        $out = array();
531
532        // copy values into vcard object
533        $vcard = new rcube_vcard($record['vcard'] ? $record['vcard'] : $save_data['vcard']);
534        $vcard->reset();
535        foreach ($save_data as $key => $values) {
536            list($field, $section) = explode(':', $key);
537            foreach ((array)$values as $value) {
538                if (isset($value))
539                    $vcard->set($field, $value, $section);
540            }
541        }
542        $out['vcard'] = $vcard->export();
543
544        foreach ($this->table_cols as $col) {
545            $key = $col;
546            if (!isset($save_data[$key]))
547                $key .= ':home';
548            if (isset($save_data[$key]))
549                $out[$col] = is_array($save_data[$key]) ? join(',', $save_data[$key]) : $save_data[$key];
550        }
551
552        // save all e-mails in database column
553        $out['email'] = join(", ", $vcard->email);
554
555        return $out;
556    }
557
558
559    /**
560     * Mark one or more contact records as deleted
561     *
562     * @param array  Record identifiers
563     */
564    function delete($ids)
565    {
566        if (!is_array($ids))
567            $ids = explode(',', $ids);
568
569        $ids = $this->db->array2list($ids, 'integer');
570
571        // flag record as deleted
572        $this->db->query(
573            "UPDATE ".get_table_name($this->db_name).
574            " SET del=1, changed=".$this->db->now().
575            " WHERE user_id=?".
576                " AND contact_id IN ($ids)",
577            $this->user_id
578        );
579
580        $this->cache = null;
581
582        return $this->db->affected_rows();
583    }
584
585
586    /**
587     * Remove all records from the database
588     */
589    function delete_all()
590    {
591        $this->db->query("DELETE FROM ".get_table_name($this->db_name)." WHERE user_id = ?", $this->user_id);
592        $this->cache = null;
593        return $this->db->affected_rows();
594    }
595
596
597    /**
598     * Create a contact group with the given name
599     *
600     * @param string The group name
601     * @return mixed False on error, array with record props in success
602     */
603    function create_group($name)
604    {
605        $result = false;
606
607        // make sure we have a unique name
608        $name = $this->unique_groupname($name);
609
610        $this->db->query(
611            "INSERT INTO ".get_table_name($this->db_groups).
612            " (user_id, changed, name)".
613            " VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
614        );
615
616        if ($insert_id = $this->db->insert_id($this->db_groups))
617            $result = array('id' => $insert_id, 'name' => $name);
618
619        return $result;
620    }
621
622
623    /**
624     * Delete the given group (and all linked group members)
625     *
626     * @param string Group identifier
627     * @return boolean True on success, false if no data was changed
628     */
629    function delete_group($gid)
630    {
631        // flag group record as deleted
632        $sql_result = $this->db->query(
633            "UPDATE ".get_table_name($this->db_groups).
634            " SET del=1, changed=".$this->db->now().
635            " WHERE contactgroup_id=?",
636            $gid
637        );
638
639        $this->cache = null;
640
641        return $this->db->affected_rows();
642    }
643
644
645    /**
646     * Rename a specific contact group
647     *
648     * @param string Group identifier
649     * @param string New name to set for this group
650     * @return boolean New name on success, false if no data was changed
651     */
652    function rename_group($gid, $newname)
653    {
654        // make sure we have a unique name
655        $name = $this->unique_groupname($newname);
656
657        $sql_result = $this->db->query(
658            "UPDATE ".get_table_name($this->db_groups).
659            " SET name=?, changed=".$this->db->now().
660            " WHERE contactgroup_id=?",
661            $name, $gid
662        );
663
664        return $this->db->affected_rows() ? $name : false;
665    }
666
667
668    /**
669     * Add the given contact records the a certain group
670     *
671     * @param string  Group identifier
672     * @param array   List of contact identifiers to be added
673     * @return int    Number of contacts added
674     */
675    function add_to_group($group_id, $ids)
676    {
677        if (!is_array($ids))
678            $ids = explode(',', $ids);
679
680        $added = 0;
681        $exists = array();
682
683        // get existing assignments ...
684        $sql_result = $this->db->query(
685            "SELECT contact_id FROM ".get_table_name($this->db_groupmembers).
686            " WHERE contactgroup_id=?".
687                " AND contact_id IN (".$this->db->array2list($ids, 'integer').")",
688            $group_id
689        );
690        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
691            $exists[] = $sql_arr['contact_id'];
692        }
693        // ... and remove them from the list
694        $ids = array_diff($ids, $exists);
695
696        foreach ($ids as $contact_id) {
697            $this->db->query(
698                "INSERT INTO ".get_table_name($this->db_groupmembers).
699                " (contactgroup_id, contact_id, created)".
700                " VALUES (?, ?, ".$this->db->now().")",
701                $group_id,
702                $contact_id
703            );
704
705            if (!$this->db->db_error)
706                $added++;
707        }
708
709        return $added;
710    }
711
712
713    /**
714     * Remove the given contact records from a certain group
715     *
716     * @param string  Group identifier
717     * @param array   List of contact identifiers to be removed
718     * @return int    Number of deleted group members
719     */
720    function remove_from_group($group_id, $ids)
721    {
722        if (!is_array($ids))
723            $ids = explode(',', $ids);
724
725        $ids = $this->db->array2list($ids, 'integer');
726
727        $sql_result = $this->db->query(
728            "DELETE FROM ".get_table_name($this->db_groupmembers).
729            " WHERE contactgroup_id=?".
730                " AND contact_id IN ($ids)",
731            $group_id
732        );
733
734        return $this->db->affected_rows();
735    }
736
737
738    /**
739     * Check for existing groups with the same name
740     *
741     * @param string Name to check
742     * @return string A group name which is unique for the current use
743     */
744    private function unique_groupname($name)
745    {
746        $checkname = $name;
747        $num = 2; $hit = false;
748
749        do {
750            $sql_result = $this->db->query(
751                "SELECT 1 FROM ".get_table_name($this->db_groups).
752                " WHERE del<>1".
753                    " AND user_id=?".
754                    " AND name=?",
755                $this->user_id,
756                $checkname);
757
758            // append number to make name unique
759            if ($hit = $this->db->num_rows($sql_result))
760                $checkname = $name . ' ' . $num++;
761        } while ($hit > 0);
762
763        return $checkname;
764    }
765
766}
Note: See TracBrowser for help on using the repository browser.