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

Last change on this file since 5708 was 5708, checked in by alec, 17 months ago
  • Fix strict email address searching if contact has more than one address
  • Property svn:keywords set to Id
File size: 31.9 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-2011, 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 $cache;
45    private $table_cols = array('name', 'email', 'firstname', 'surname');
46    private $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'nickname',
47      'jobtitle', 'organization', 'department', 'maidenname', 'email', 'phone',
48      'address', 'street', 'locality', 'zipcode', 'region', 'country', 'website', 'im', 'notes');
49
50    // public properties
51    public $primary_key = 'contact_id';
52    public $name;
53    public $readonly = false;
54    public $groups = true;
55    public $undelete = true;
56    public $list_page = 1;
57    public $page_size = 10;
58    public $group_id = 0;
59    public $ready = false;
60    public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname',
61      'jobtitle', 'organization', 'department', 'assistant', 'manager',
62      'gender', 'maidenname', 'spouse', 'email', 'phone', 'address',
63      'birthday', 'anniversary', 'website', 'im', 'notes', 'photo');
64
65    const SEPARATOR = ',';
66
67
68    /**
69     * Object constructor
70     *
71     * @param object  Instance of the rcube_db class
72     * @param integer User-ID
73     */
74    function __construct($dbconn, $user)
75    {
76        $this->db = $dbconn;
77        $this->user_id = $user;
78        $this->ready = $this->db && !$this->db->is_error();
79    }
80
81
82    /**
83     * Returns addressbook name
84     */
85     function get_name()
86     {
87        return $this->name;
88     }
89
90
91    /**
92     * Save a search string for future listings
93     *
94     * @param string SQL params to use in listing method
95     */
96    function set_search_set($filter)
97    {
98        $this->filter = $filter;
99        $this->cache = null;
100    }
101
102
103    /**
104     * Getter for saved search properties
105     *
106     * @return mixed Search properties used by this class
107     */
108    function get_search_set()
109    {
110        return $this->filter;
111    }
112
113
114    /**
115     * Setter for the current group
116     * (empty, has to be re-implemented by extending class)
117     */
118    function set_group($gid)
119    {
120        $this->group_id = $gid;
121        $this->cache = null;
122    }
123
124
125    /**
126     * Reset all saved results and search parameters
127     */
128    function reset()
129    {
130        $this->result = null;
131        $this->filter = null;
132        $this->cache = null;
133    }
134
135
136    /**
137     * List all active contact groups of this source
138     *
139     * @param string  Search string to match group name
140     * @return array  Indexed list of contact groups, each a hash array
141     */
142    function list_groups($search = null)
143    {
144        $results = array();
145
146        if (!$this->groups)
147            return $results;
148
149        $sql_filter = $search ? " AND " . $this->db->ilike('name', '%'.$search.'%') : '';
150
151        $sql_result = $this->db->query(
152            "SELECT * FROM ".get_table_name($this->db_groups).
153            " WHERE del<>1".
154            " AND user_id=?".
155            $sql_filter.
156            " ORDER BY name",
157            $this->user_id);
158
159        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
160            $sql_arr['ID'] = $sql_arr['contactgroup_id'];
161            $results[]     = $sql_arr;
162        }
163
164        return $results;
165    }
166
167
168    /**
169     * Get group properties such as name and email address(es)
170     *
171     * @param string Group identifier
172     * @return array Group properties as hash array
173     */
174    function get_group($group_id)
175    {
176        $sql_result = $this->db->query(
177            "SELECT * FROM ".get_table_name($this->db_groups).
178            " WHERE del<>1".
179            " AND contactgroup_id=?".
180            " AND user_id=?",
181            $group_id, $this->user_id);
182
183        if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
184            $sql_arr['ID'] = $sql_arr['contactgroup_id'];
185            return $sql_arr;
186        }
187
188        return null;
189    }
190
191    /**
192     * List the current set of contact records
193     *
194     * @param  array   List of cols to show, Null means all
195     * @param  int     Only return this number of records, use negative values for tail
196     * @param  boolean True to skip the count query (select only)
197     * @return array  Indexed list of contact records, each a hash array
198     */
199    function list_records($cols=null, $subset=0, $nocount=false)
200    {
201        if ($nocount || $this->list_page <= 1) {
202            // create dummy result, we don't need a count now
203            $this->result = new rcube_result_set();
204        } else {
205            // count all records
206            $this->result = $this->count();
207        }
208
209        $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
210        $length = $subset != 0 ? abs($subset) : $this->page_size;
211
212        if ($this->group_id)
213            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
214                " ON (m.contact_id = c.".$this->primary_key.")";
215
216        $sql_result = $this->db->limitquery(
217            "SELECT * FROM ".get_table_name($this->db_name)." AS c" .
218            $join .
219            " WHERE c.del<>1" .
220                " AND c.user_id=?" .
221                ($this->group_id ? " AND m.contactgroup_id=?" : "").
222                ($this->filter ? " AND (".$this->filter.")" : "") .
223            " ORDER BY ". $this->db->concat('c.name', 'c.email'),
224            $start_row,
225            $length,
226            $this->user_id,
227            $this->group_id);
228
229        // determine whether we have to parse the vcard or if only db cols are requested
230        $read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols);
231
232        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
233            $sql_arr['ID'] = $sql_arr[$this->primary_key];
234
235            if ($read_vcard)
236                $sql_arr = $this->convert_db_data($sql_arr);
237            else {
238                $sql_arr['email'] = explode(self::SEPARATOR, $sql_arr['email']);
239                $sql_arr['email'] = array_map('trim', $sql_arr['email']);
240            }
241
242            // make sure we have a name to display
243            if (empty($sql_arr['name'])) {
244                if (empty($sql_arr['email']))
245                  $sql_arr['email'] = $this->get_col_values('email', $sql_arr, true);
246                $sql_arr['name'] = $sql_arr['email'][0];
247            }
248
249            $this->result->add($sql_arr);
250        }
251
252        $cnt = count($this->result->records);
253
254        // update counter
255        if ($nocount)
256            $this->result->count = $cnt;
257        else if ($this->list_page <= 1) {
258            if ($cnt < $this->page_size && $subset == 0)
259                $this->result->count = $cnt;
260            else if (isset($this->cache['count']))
261                $this->result->count = $this->cache['count'];
262            else
263                $this->result->count = $this->_count();
264        }
265
266        return $this->result;
267    }
268
269
270    /**
271     * Search contacts
272     *
273     * @param mixed   $fields   The field name of array of field names to search in
274     * @param mixed   $value    Search value (or array of values when $fields is array)
275     * @param int     $mode     Matching mode:
276     *                          0 - partial (*abc*),
277     *                          1 - strict (=),
278     *                          2 - prefix (abc*)
279     * @param boolean $select   True if results are requested, False if count only
280     * @param boolean $nocount  True to skip the count query (select only)
281     * @param array   $required List of fields that cannot be empty
282     *
283     * @return object rcube_result_set Contact records and 'count' value
284     */
285    function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
286    {
287        if (!is_array($fields))
288            $fields = array($fields);
289        if (!is_array($required) && !empty($required))
290            $required = array($required);
291
292        $where = $and_where = array();
293        $mode = intval($mode);
294        $WS = ' ';
295        $AS = self::SEPARATOR;
296
297        foreach ($fields as $idx => $col) {
298            // direct ID search
299            if ($col == 'ID' || $col == $this->primary_key) {
300                $ids     = !is_array($value) ? explode(self::SEPARATOR, $value) : $value;
301                $ids     = $this->db->array2list($ids, 'integer');
302                $where[] = 'c.' . $this->primary_key.' IN ('.$ids.')';
303                continue;
304            }
305            // fulltext search in all fields
306            else if ($col == '*') {
307                $words = array();
308                foreach (explode($WS, self::normalize_string($value)) as $word) {
309                    switch ($mode) {
310                    case 1: // strict
311                        $words[] = '(' . $this->db->ilike('words', $word . '%')
312                            . ' OR ' . $this->db->ilike('words', '%' . $WS . $word . $WS . '%')
313                            . ' OR ' . $this->db->ilike('words', '%' . $WS . $word) . ')';
314                        break;
315                    case 2: // prefix
316                        $words[] = '(' . $this->db->ilike('words', $word . '%')
317                            . ' OR ' . $this->db->ilike('words', '%' . $WS . $word . '%') . ')';
318                        break;
319                    default: // partial
320                        $words[] = $this->db->ilike('words', '%' . $word . '%');
321                    }
322                }
323                $where[] = '(' . join(' AND ', $words) . ')';
324            }
325            else {
326                $val = is_array($value) ? $value[$idx] : $value;
327                // table column
328                if (in_array($col, $this->table_cols)) {
329                    switch ($mode) {
330                    case 1: // strict
331                        $where[] = '(' . $this->db->quoteIdentifier($col) . ' = ' . $this->db->quote($val)
332                            . ' OR ' . $this->db->ilike($col, $val . $AS . '%')
333                            . ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%')
334                            . ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')';
335                        break;
336                    case 2: // prefix
337                        $where[] = '(' . $this->db->ilike($col, $val . '%')
338                            . ' OR ' . $this->db->ilike($col, $AS . $val . '%') . ')';
339                        break;
340                    default: // partial
341                        $where[] = $this->db->ilike($col, '%' . $val . '%');
342                    }
343                }
344                // vCard field
345                else {
346                    if (in_array($col, $this->fulltext_cols)) {
347                        foreach (explode(" ", self::normalize_string($val)) as $word) {
348                            switch ($mode) {
349                            case 1: // strict
350                                $words[] = '(' . $this->db->ilike('words', $word . $WS . '%')
351                                    . ' OR ' . $this->db->ilike('words', '%' . $AS . $word . $WS .'%')
352                                    . ' OR ' . $this->db->ilike('words', '%' . $AS . $word) . ')';
353                                break;
354                            case 2: // prefix
355                                $words[] = '(' . $this->db->ilike('words', $word . '%')
356                                    . ' OR ' . $this->db->ilike('words', $AS . $word . '%') . ')';
357                                break;
358                            default: // partial
359                                $words[] = $this->db->ilike('words', '%' . $word . '%');
360                            }
361                        }
362                        $where[] = '(' . join(' AND ', $words) . ')';
363                    }
364                    if (is_array($value))
365                        $post_search[$col] = mb_strtolower($val);
366                }
367            }
368        }
369
370        foreach (array_intersect($required, $this->table_cols) as $col) {
371            $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote('');
372        }
373
374        if (!empty($where)) {
375            // use AND operator for advanced searches
376            $where = join(is_array($value) ? ' AND ' : ' OR ', $where);
377        }
378
379        if (!empty($and_where))
380            $where = ($where ? "($where) AND " : '') . join(' AND ', $and_where);
381
382        // Post-searching in vCard data fields
383        // we will search in all records and then build a where clause for their IDs
384        if (!empty($post_search)) {
385            $ids = array(0);
386            // build key name regexp
387            $regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/';
388            // use initial WHERE clause, to limit records number if possible
389            if (!empty($where))
390                $this->set_search_set($where);
391
392            // count result pages
393            $cnt   = $this->count();
394            $pages = ceil($cnt / $this->page_size);
395            $scnt  = count($post_search);
396
397            // get (paged) result
398            for ($i=0; $i<$pages; $i++) {
399                $this->list_records(null, $i, true);
400                while ($row = $this->result->next()) {
401                    $id = $row[$this->primary_key];
402                    $found = array();
403                    foreach (preg_grep($regexp, array_keys($row)) as $col) {
404                        $pos     = strpos($col, ':');
405                        $colname = $pos ? substr($col, 0, $pos) : $col;
406                        $search  = $post_search[$colname];
407                        foreach ((array)$row[$col] as $value) {
408                            // composite field, e.g. address
409                            foreach ((array)$value as $val) {
410                                $val = mb_strtolower($val);
411                                switch ($mode) {
412                                case 1:
413                                    $got = ($val == $search);
414                                    break;
415                                case 2:
416                                    $got = ($search == substr($val, 0, strlen($search)));
417                                    break;
418                                default:
419                                    $got = (strpos($val, $search) !== false);
420                                    break;
421                                }
422
423                                if ($got) {
424                                    $found[$colname] = true;
425                                    break 2;
426                                }
427                            }
428                        }
429                    }
430                    // all fields match
431                    if (count($found) >= $scnt) {
432                        $ids[] = $id;
433                    }
434                }
435            }
436
437            // build WHERE clause
438            $ids = $this->db->array2list($ids, 'integer');
439            $where = 'c.' . $this->primary_key.' IN ('.$ids.')';
440            // reset counter
441            unset($this->cache['count']);
442
443            // when we know we have an empty result
444            if ($ids == '0') {
445                $this->set_search_set($where);
446                return ($this->result = new rcube_result_set(0, 0));
447            }
448        }
449
450        if (!empty($where)) {
451            $this->set_search_set($where);
452            if ($select)
453                $this->list_records(null, 0, $nocount);
454            else
455                $this->result = $this->count();
456        }
457
458        return $this->result;
459    }
460
461
462    /**
463     * Count number of available contacts in database
464     *
465     * @return rcube_result_set Result object
466     */
467    function count()
468    {
469        $count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count();
470
471        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
472    }
473
474
475    /**
476     * Count number of available contacts in database
477     *
478     * @return int Contacts count
479     */
480    private function _count()
481    {
482        if ($this->group_id)
483            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
484                " ON (m.contact_id=c.".$this->primary_key.")";
485
486        // count contacts for this user
487        $sql_result = $this->db->query(
488            "SELECT COUNT(c.contact_id) AS rows".
489            " FROM ".get_table_name($this->db_name)." AS c".
490                $join.
491            " WHERE c.del<>1".
492            " AND c.user_id=?".
493            ($this->group_id ? " AND m.contactgroup_id=?" : "").
494            ($this->filter ? " AND (".$this->filter.")" : ""),
495            $this->user_id,
496            $this->group_id
497        );
498
499        $sql_arr = $this->db->fetch_assoc($sql_result);
500
501        $this->cache['count'] = (int) $sql_arr['rows'];
502
503        return $this->cache['count'];
504    }
505
506
507    /**
508     * Return the last result set
509     *
510     * @return mixed Result array or NULL if nothing selected yet
511     */
512    function get_result()
513    {
514        return $this->result;
515    }
516
517
518    /**
519     * Get a specific contact record
520     *
521     * @param mixed record identifier(s)
522     * @return mixed Result object with all record fields or False if not found
523     */
524    function get_record($id, $assoc=false)
525    {
526        // return cached result
527        if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id)
528            return $assoc ? $first : $this->result;
529
530        $this->db->query(
531            "SELECT * FROM ".get_table_name($this->db_name).
532            " WHERE contact_id=?".
533                " AND user_id=?".
534                " AND del<>1",
535            $id,
536            $this->user_id
537        );
538
539        if ($sql_arr = $this->db->fetch_assoc()) {
540            $record = $this->convert_db_data($sql_arr);
541            $this->result = new rcube_result_set(1);
542            $this->result->add($record);
543        }
544
545        return $assoc && $record ? $record : $this->result;
546    }
547
548
549    /**
550     * Get group assignments of a specific contact record
551     *
552     * @param mixed Record identifier
553     * @return array List of assigned groups as ID=>Name pairs
554     */
555    function get_record_groups($id)
556    {
557      $results = array();
558
559      if (!$this->groups)
560          return $results;
561
562      $sql_result = $this->db->query(
563        "SELECT cgm.contactgroup_id, cg.name FROM " . get_table_name($this->db_groupmembers) . " AS cgm" .
564        " LEFT JOIN " . get_table_name($this->db_groups) . " AS cg ON (cgm.contactgroup_id = cg.contactgroup_id AND cg.del<>1)" .
565        " WHERE cgm.contact_id=?",
566        $id
567      );
568      while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
569        $results[$sql_arr['contactgroup_id']] = $sql_arr['name'];
570      }
571
572      return $results;
573    }
574
575
576    /**
577     * Check the given data before saving.
578     * If input not valid, the message to display can be fetched using get_error()
579     *
580     * @param array Assoziative array with data to save
581     * @param boolean Try to fix/complete record automatically
582     * @return boolean True if input is valid, False if not.
583     */
584    public function validate(&$save_data, $autofix = false)
585    {
586        // validate e-mail addresses
587        $valid = parent::validate($save_data, $autofix);
588
589        // require at least one e-mail address (syntax check is already done)
590        if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) {
591            $this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
592            $valid = false;
593        }
594
595        return $valid;
596    }
597
598
599    /**
600     * Create a new contact record
601     *
602     * @param array Associative array with save data
603     * @return integer|boolean The created record ID on success, False on error
604     */
605    function insert($save_data, $check=false)
606    {
607        if (!is_array($save_data))
608            return false;
609
610        $insert_id = $existing = false;
611
612        if ($check) {
613            foreach ($save_data as $col => $values) {
614                if (strpos($col, 'email') === 0) {
615                    foreach ((array)$values as $email) {
616                        if ($existing = $this->search('email', $email, false, false))
617                            break 2;
618                    }
619                }
620            }
621        }
622
623        $save_data = $this->convert_save_data($save_data);
624        $a_insert_cols = $a_insert_values = array();
625
626        foreach ($save_data as $col => $value) {
627            $a_insert_cols[]   = $this->db->quoteIdentifier($col);
628            $a_insert_values[] = $this->db->quote($value);
629        }
630
631        if (!$existing->count && !empty($a_insert_cols)) {
632            $this->db->query(
633                "INSERT INTO ".get_table_name($this->db_name).
634                " (user_id, changed, del, ".join(', ', $a_insert_cols).")".
635                " VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
636            );
637
638            $insert_id = $this->db->insert_id($this->db_name);
639        }
640
641        // also add the newly created contact to the active group
642        if ($insert_id && $this->group_id)
643            $this->add_to_group($this->group_id, $insert_id);
644
645        $this->cache = null;
646
647        return $insert_id;
648    }
649
650
651    /**
652     * Update a specific contact record
653     *
654     * @param mixed Record identifier
655     * @param array Assoziative array with save data
656     * @return boolean True on success, False on error
657     */
658    function update($id, $save_cols)
659    {
660        $updated = false;
661        $write_sql = array();
662        $record = $this->get_record($id, true);
663        $save_cols = $this->convert_save_data($save_cols, $record);
664
665        foreach ($save_cols as $col => $value) {
666            $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
667        }
668
669        if (!empty($write_sql)) {
670            $this->db->query(
671                "UPDATE ".get_table_name($this->db_name).
672                " SET changed=".$this->db->now().", ".join(', ', $write_sql).
673                " WHERE contact_id=?".
674                    " AND user_id=?".
675                    " AND del<>1",
676                $id,
677                $this->user_id
678            );
679
680            $updated = $this->db->affected_rows();
681            $this->result = null;  // clear current result (from get_record())
682        }
683
684        return $updated;
685    }
686
687
688    private function convert_db_data($sql_arr)
689    {
690        $record = array();
691        $record['ID'] = $sql_arr[$this->primary_key];
692
693        if ($sql_arr['vcard']) {
694            unset($sql_arr['email']);
695            $vcard = new rcube_vcard($sql_arr['vcard']);
696            $record += $vcard->get_assoc() + $sql_arr;
697        }
698        else {
699            $record += $sql_arr;
700            $record['email'] = explode(self::SEPARATOR, $record['email']);
701            $record['email'] = array_map('trim', $record['email']);
702        }
703
704        return $record;
705    }
706
707
708    private function convert_save_data($save_data, $record = array())
709    {
710        $out = array();
711        $words = '';
712
713        // copy values into vcard object
714        $vcard = new rcube_vcard($record['vcard'] ? $record['vcard'] : $save_data['vcard']);
715        $vcard->reset();
716        foreach ($save_data as $key => $values) {
717            list($field, $section) = explode(':', $key);
718            $fulltext = in_array($field, $this->fulltext_cols);
719            foreach ((array)$values as $value) {
720                if (isset($value))
721                    $vcard->set($field, $value, $section);
722                if ($fulltext && is_array($value))
723                    $words .= ' ' . self::normalize_string(join(" ", $value));
724                else if ($fulltext && strlen($value) >= 3)
725                    $words .= ' ' . self::normalize_string($value);
726            }
727        }
728        $out['vcard'] = $vcard->export(false);
729
730        foreach ($this->table_cols as $col) {
731            $key = $col;
732            if (!isset($save_data[$key]))
733                $key .= ':home';
734            if (isset($save_data[$key])) {
735                if (is_array($save_data[$key]))
736                    $out[$col] = join(self::SEPARATOR, $save_data[$key]);
737                else
738                    $out[$col] = $save_data[$key];
739            }
740        }
741
742        // save all e-mails in database column
743        $out['email'] = join(self::SEPARATOR, $vcard->email);
744
745        // join words for fulltext search
746        $out['words'] = join(" ", array_unique(explode(" ", $words)));
747
748        return $out;
749    }
750
751
752    /**
753     * Mark one or more contact records as deleted
754     *
755     * @param array   Record identifiers
756     * @param boolean Remove record(s) irreversible (unsupported)
757     */
758    function delete($ids, $force=true)
759    {
760        if (!is_array($ids))
761            $ids = explode(self::SEPARATOR, $ids);
762
763        $ids = $this->db->array2list($ids, 'integer');
764
765        // flag record as deleted (always)
766        $this->db->query(
767            "UPDATE ".get_table_name($this->db_name).
768            " SET del=1, changed=".$this->db->now().
769            " WHERE user_id=?".
770                " AND contact_id IN ($ids)",
771            $this->user_id
772        );
773
774        $this->cache = null;
775
776        return $this->db->affected_rows();
777    }
778
779
780    /**
781     * Undelete one or more contact records
782     *
783     * @param array  Record identifiers
784     */
785    function undelete($ids)
786    {
787        if (!is_array($ids))
788            $ids = explode(self::SEPARATOR, $ids);
789
790        $ids = $this->db->array2list($ids, 'integer');
791
792        // clear deleted flag
793        $this->db->query(
794            "UPDATE ".get_table_name($this->db_name).
795            " SET del=0, changed=".$this->db->now().
796            " WHERE user_id=?".
797                " AND contact_id IN ($ids)",
798            $this->user_id
799        );
800
801        $this->cache = null;
802
803        return $this->db->affected_rows();
804    }
805
806
807    /**
808     * Remove all records from the database
809     */
810    function delete_all()
811    {
812        $this->cache = null;
813
814        $this->db->query("UPDATE ".get_table_name($this->db_name).
815            " SET del=1, changed=".$this->db->now().
816            " WHERE user_id = ?", $this->user_id);
817
818        return $this->db->affected_rows();
819    }
820
821
822    /**
823     * Create a contact group with the given name
824     *
825     * @param string The group name
826     * @return mixed False on error, array with record props in success
827     */
828    function create_group($name)
829    {
830        $result = false;
831
832        // make sure we have a unique name
833        $name = $this->unique_groupname($name);
834
835        $this->db->query(
836            "INSERT INTO ".get_table_name($this->db_groups).
837            " (user_id, changed, name)".
838            " VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
839        );
840
841        if ($insert_id = $this->db->insert_id($this->db_groups))
842            $result = array('id' => $insert_id, 'name' => $name);
843
844        return $result;
845    }
846
847
848    /**
849     * Delete the given group (and all linked group members)
850     *
851     * @param string Group identifier
852     * @return boolean True on success, false if no data was changed
853     */
854    function delete_group($gid)
855    {
856        // flag group record as deleted
857        $sql_result = $this->db->query(
858            "UPDATE ".get_table_name($this->db_groups).
859            " SET del=1, changed=".$this->db->now().
860            " WHERE contactgroup_id=?".
861            " AND user_id=?",
862            $gid, $this->user_id
863        );
864
865        $this->cache = null;
866
867        return $this->db->affected_rows();
868    }
869
870
871    /**
872     * Rename a specific contact group
873     *
874     * @param string Group identifier
875     * @param string New name to set for this group
876     * @return boolean New name on success, false if no data was changed
877     */
878    function rename_group($gid, $newname)
879    {
880        // make sure we have a unique name
881        $name = $this->unique_groupname($newname);
882
883        $sql_result = $this->db->query(
884            "UPDATE ".get_table_name($this->db_groups).
885            " SET name=?, changed=".$this->db->now().
886            " WHERE contactgroup_id=?".
887            " AND user_id=?",
888            $name, $gid, $this->user_id
889        );
890
891        return $this->db->affected_rows() ? $name : false;
892    }
893
894
895    /**
896     * Add the given contact records the a certain group
897     *
898     * @param string  Group identifier
899     * @param array   List of contact identifiers to be added
900     * @return int    Number of contacts added
901     */
902    function add_to_group($group_id, $ids)
903    {
904        if (!is_array($ids))
905            $ids = explode(self::SEPARATOR, $ids);
906
907        $added = 0;
908        $exists = array();
909
910        // get existing assignments ...
911        $sql_result = $this->db->query(
912            "SELECT contact_id FROM ".get_table_name($this->db_groupmembers).
913            " WHERE contactgroup_id=?".
914                " AND contact_id IN (".$this->db->array2list($ids, 'integer').")",
915            $group_id
916        );
917        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
918            $exists[] = $sql_arr['contact_id'];
919        }
920        // ... and remove them from the list
921        $ids = array_diff($ids, $exists);
922
923        foreach ($ids as $contact_id) {
924            $this->db->query(
925                "INSERT INTO ".get_table_name($this->db_groupmembers).
926                " (contactgroup_id, contact_id, created)".
927                " VALUES (?, ?, ".$this->db->now().")",
928                $group_id,
929                $contact_id
930            );
931
932            if (!$this->db->db_error)
933                $added++;
934        }
935
936        return $added;
937    }
938
939
940    /**
941     * Remove the given contact records from a certain group
942     *
943     * @param string  Group identifier
944     * @param array   List of contact identifiers to be removed
945     * @return int    Number of deleted group members
946     */
947    function remove_from_group($group_id, $ids)
948    {
949        if (!is_array($ids))
950            $ids = explode(self::SEPARATOR, $ids);
951
952        $ids = $this->db->array2list($ids, 'integer');
953
954        $sql_result = $this->db->query(
955            "DELETE FROM ".get_table_name($this->db_groupmembers).
956            " WHERE contactgroup_id=?".
957                " AND contact_id IN ($ids)",
958            $group_id
959        );
960
961        return $this->db->affected_rows();
962    }
963
964
965    /**
966     * Check for existing groups with the same name
967     *
968     * @param string Name to check
969     * @return string A group name which is unique for the current use
970     */
971    private function unique_groupname($name)
972    {
973        $checkname = $name;
974        $num = 2; $hit = false;
975
976        do {
977            $sql_result = $this->db->query(
978                "SELECT 1 FROM ".get_table_name($this->db_groups).
979                " WHERE del<>1".
980                    " AND user_id=?".
981                    " AND name=?",
982                $this->user_id,
983                $checkname);
984
985            // append number to make name unique
986            if ($hit = $this->db->num_rows($sql_result))
987                $checkname = $name . ' ' . $num++;
988        } while ($hit > 0);
989
990        return $checkname;
991    }
992
993}
Note: See TracBrowser for help on using the repository browser.