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