source: subversion/branches/devel-addressbook/program/include/rcube_contacts.php @ 4231

Last change on this file since 4231 was 4231, checked in by thomasb, 3 years ago

Improve vcard field name assignments

  • Property svn:keywords set to Id
File size: 20.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-2010, Roundcube Dev. - Switzerland                 |
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    var $primary_key = 'contact_id';
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;
57
58
59    /**
60     * Object constructor
61     *
62     * @param object  Instance of the rcube_db class
63     * @param integer User-ID
64     */
65    function __construct($dbconn, $user)
66    {
67        $this->db = $dbconn;
68        $this->user_id = $user;
69        $this->ready = $this->db && !$this->db->is_error();
70    }
71
72
73    /**
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)
79    {
80        $this->filter = $filter;
81        $this->cache = null;
82    }
83
84
85    /**
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;
103        $this->cache = null;
104    }
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;
116        $this->cache = null;
117    }
118
119
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();
129
130        if (!$this->groups)
131            return $results;
132
133        $sql_filter = $search ? " AND " . $this->db->ilike('name', '%'.$search.'%') : '';
134
135        $sql_result = $this->db->query(
136            "SELECT * FROM ".get_table_name($this->db_groups).
137            " WHERE del<>1".
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)
174            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
175                " ON (m.contact_id = c.".$this->primary_key.")";
176
177        $sql_result = $this->db->limitquery(
178            "SELECT * FROM ".get_table_name($this->db_name)." AS c" .
179            $join .
180            " WHERE c.del<>1" .
181                " AND c.user_id=?" .
182                ($this->group_id ? " AND m.contactgroup_id=?" : "").
183                ($this->filter ? " AND (".$this->filter.")" : "") .
184            " ORDER BY c.name",
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        }
197
198        $cnt = count($this->result->records);
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;
206            else if (isset($this->cache['count']))
207                $this->result->count = $this->cache['count'];
208            else
209                $this->result->count = $this->_count();
210        }
211
212        return $this->result;
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)
224     * @param array   List of fields that cannot be empty
225     * @return Indexed list of contact records and 'count' value
226     */
227    function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
228    {
229        if (!is_array($fields))
230            $fields = array($fields);
231        if (!is_array($required) && !empty($required))
232            $required = array($required);
233
234        $where = $and_where = array();
235
236        foreach ($fields as $col) {
237            if ($col == 'ID' || $col == $this->primary_key) {
238                $ids     = !is_array($value) ? explode(',', $value) : $value;
239                $ids     = $this->db->array2list($ids, 'integer');
240                $where[] = 'c.' . $this->primary_key.' IN ('.$ids.')';
241            }
242            else if ($strict)
243                $where[] = $this->db->quoteIdentifier($col).' = '.$this->db->quote($value);
244            else
245                $where[] = $this->db->ilike($col, '%'.$value.'%');
246        }
247
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);
260            if ($select)
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    {
277        $count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count();
278
279        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
280    }
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)
291            $join = " LEFT JOIN ".get_table_name($this->db_groupmembers)." AS m".
292                " ON (m.contact_id=c.".$this->primary_key.")";
293
294        // count contacts for this user
295        $sql_result = $this->db->query(
296            "SELECT COUNT(c.contact_id) AS rows".
297            " FROM ".get_table_name($this->db_name)." AS c".
298                $join.
299            " WHERE c.del<>1".
300            " AND c.user_id=?".
301            ($this->group_id ? " AND m.contactgroup_id=?" : "").
302            ($this->filter ? " AND (".$this->filter.")" : ""),
303            $this->user_id,
304            $this->group_id
305        );
306
307        $sql_arr = $this->db->fetch_assoc($sql_result);
308
309        $this->cache['count'] = (int) $sql_arr['rows'];
310
311        return $this->cache['count'];
312    }
313
314
315    /**
316     * Return the last result set
317     *
318     * @return mixed Result array or NULL if nothing selected yet
319     */
320    function get_result()
321    {
322        return $this->result;
323    }
324
325
326    /**
327     * Get a specific contact record
328     *
329     * @param mixed record identifier(s)
330     * @return mixed Result object with all record fields or False if not found
331     */
332    function get_record($id, $assoc=false)
333    {
334        // return cached result
335        if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id)
336            return $assoc ? $first : $this->result;
337
338        $this->db->query(
339            "SELECT * FROM ".get_table_name($this->db_name).
340            " WHERE contact_id=?".
341                " AND user_id=?".
342                " AND del<>1",
343            $id,
344            $this->user_id
345        );
346
347        if ($sql_arr = $this->db->fetch_assoc()) {
348            $record = $this->convert_db_data($sql_arr);
349            $this->result = new rcube_result_set(1);
350            $this->result->add($record);
351        }
352
353        return $assoc && $record ? $record : $this->result;
354    }
355
356
357    /**
358     * Get group assignments of a specific contact record
359     *
360     * @param mixed Record identifier
361     * @return 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    /**
385     * Create a new contact record
386     *
387     * @param array Associative array with save data
388     * @return integer|boolean The created record ID on success, False on error
389     */
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        $save_data = $this->convert_save_data($save_data);
401        $a_insert_cols = $a_insert_values = array();
402
403        foreach ($save_data as $col => $value) {
404            $a_insert_cols[]   = $this->db->quoteIdentifier($col);
405            $a_insert_values[] = $this->db->quote($value);
406        }
407
408        if (!$existing->count && !empty($a_insert_cols)) {
409            $this->db->query(
410                "INSERT INTO ".get_table_name($this->db_name).
411                " (user_id, changed, del, ".join(', ', $a_insert_cols).")".
412                " VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
413            );
414
415            $insert_id = $this->db->insert_id($this->db_name);
416        }
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
422        $this->cache = null;
423
424        return $insert_id;
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
447     * @return boolean True on success, False on error
448     */
449    function update($id, $save_cols)
450    {
451        $updated = false;
452        $write_sql = array();
453        $record = $this->get_record($id, true);
454        $save_cols = $this->convert_save_data($save_cols, $record);
455
456        foreach ($save_cols as $col => $value) {
457            $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
458        }
459
460        if (!empty($write_sql)) {
461            $this->db->query(
462                "UPDATE ".get_table_name($this->db_name).
463                " SET changed=".$this->db->now().", ".join(', ', $write_sql).
464                " WHERE contact_id=?".
465                    " AND user_id=?".
466                    " AND del<>1",
467                $id,
468                $this->user_id
469            );
470
471            $updated = $this->db->affected_rows();
472        }
473
474        return $updated;
475    }
476   
477   
478    private function convert_db_data($sql_arr)
479    {
480        $record = array();
481        $record['ID'] = $sql_arr[$this->primary_key];
482       
483        if ($sql_arr['vcard']) {
484          $vcard = new rcube_vcard($sql_arr['vcard']);
485          $record += $vcard->get_assoc() + $sql_arr;
486        }
487        else
488          $record += $sql_arr;
489       
490        return $record;
491    }
492
493
494    private function convert_save_data($save_data, $record = array())
495    {
496        $out = array();
497
498        // copy values into vcard object
499        $vcard = new rcube_vcard($record['vcard']);
500        foreach ($save_data as $key => $values) {
501            list($field, $section) = explode(':', $key);
502            foreach ((array)$values as $value) {
503                if (strlen($value) || is_array($value))
504                    $vcard->set($field, $value, strtoupper($section));
505            }
506        }
507        $out['vcard'] = $vcard->export();
508
509        foreach ($this->table_cols as $col) {
510            $key = $col;
511            if (!isset($save_data[$key]))
512                $key .= ':home';
513            if (isset($save_data[$key]))
514                $out[$col] = is_array($save_data[$key]) ? join(',', $save_data[$key]) : $save_data[$key];
515        }
516
517        return $out;
518    }
519
520
521    /**
522     * Mark one or more contact records as deleted
523     *
524     * @param array  Record identifiers
525     */
526    function delete($ids)
527    {
528        if (!is_array($ids))
529            $ids = explode(',', $ids);
530
531        $ids = $this->db->array2list($ids, 'integer');
532
533        // flag record as deleted
534        $this->db->query(
535            "UPDATE ".get_table_name($this->db_name).
536            " SET del=1, changed=".$this->db->now().
537            " WHERE user_id=?".
538                " AND contact_id IN ($ids)",
539            $this->user_id
540        );
541
542        $this->cache = null;
543
544        return $this->db->affected_rows();
545    }
546
547
548    /**
549     * Remove all records from the database
550     */
551    function delete_all()
552    {
553        $this->db->query("DELETE FROM ".get_table_name($this->db_name)." WHERE user_id = ?", $this->user_id);
554        $this->cache = null;
555        return $this->db->affected_rows();
556    }
557
558
559    /**
560     * Create a contact group with the given name
561     *
562     * @param string The group name
563     * @return mixed False on error, array with record props in success
564     */
565    function create_group($name)
566    {
567        $result = false;
568
569        // make sure we have a unique name
570        $name = $this->unique_groupname($name);
571
572        $this->db->query(
573            "INSERT INTO ".get_table_name($this->db_groups).
574            " (user_id, changed, name)".
575            " VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
576        );
577
578        if ($insert_id = $this->db->insert_id($this->db_groups))
579            $result = array('id' => $insert_id, 'name' => $name);
580
581        return $result;
582    }
583
584
585    /**
586     * Delete the given group (and all linked group members)
587     *
588     * @param string Group identifier
589     * @return boolean True on success, false if no data was changed
590     */
591    function delete_group($gid)
592    {
593        // flag group record as deleted
594        $sql_result = $this->db->query(
595            "UPDATE ".get_table_name($this->db_groups).
596            " SET del=1, changed=".$this->db->now().
597            " WHERE contactgroup_id=?",
598            $gid
599        );
600
601        $this->cache = null;
602
603        return $this->db->affected_rows();
604    }
605
606
607    /**
608     * Rename a specific contact group
609     *
610     * @param string Group identifier
611     * @param string New name to set for this group
612     * @return boolean New name on success, false if no data was changed
613     */
614    function rename_group($gid, $newname)
615    {
616        // make sure we have a unique name
617        $name = $this->unique_groupname($newname);
618
619        $sql_result = $this->db->query(
620            "UPDATE ".get_table_name($this->db_groups).
621            " SET name=?, changed=".$this->db->now().
622            " WHERE contactgroup_id=?",
623            $name, $gid
624        );
625
626        return $this->db->affected_rows() ? $name : false;
627    }
628
629
630    /**
631     * Add the given contact records the a certain group
632     *
633     * @param string  Group identifier
634     * @param array   List of contact identifiers to be added
635     * @return int    Number of contacts added
636     */
637    function add_to_group($group_id, $ids)
638    {
639        if (!is_array($ids))
640            $ids = explode(',', $ids);
641
642        $added = 0;
643        $exists = array();
644
645        // get existing assignments ...
646        $sql_result = $this->db->query(
647            "SELECT contact_id FROM ".get_table_name($this->db_groupmembers).
648            " WHERE contactgroup_id=?".
649                " AND contact_id IN (".$this->db->array2list($ids, 'integer').")",
650            $group_id
651        );
652        while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
653            $exists[] = $sql_arr['contact_id'];
654        }
655        // ... and remove them from the list
656        $ids = array_diff($ids, $exists);
657
658        foreach ($ids as $contact_id) {
659            $this->db->query(
660                "INSERT INTO ".get_table_name($this->db_groupmembers).
661                " (contactgroup_id, contact_id, created)".
662                " VALUES (?, ?, ".$this->db->now().")",
663                $group_id,
664                $contact_id
665            );
666
667            if (!$this->db->db_error)
668                $added++;
669        }
670
671        return $added;
672    }
673
674
675    /**
676     * Remove the given contact records from a certain group
677     *
678     * @param string  Group identifier
679     * @param array   List of contact identifiers to be removed
680     * @return int    Number of deleted group members
681     */
682    function remove_from_group($group_id, $ids)
683    {
684        if (!is_array($ids))
685            $ids = explode(',', $ids);
686
687        $ids = $this->db->array2list($ids, 'integer');
688
689        $sql_result = $this->db->query(
690            "DELETE FROM ".get_table_name($this->db_groupmembers).
691            " WHERE contactgroup_id=?".
692                " AND contact_id IN ($ids)",
693            $group_id
694        );
695
696        return $this->db->affected_rows();
697    }
698
699
700    /**
701     * Check for existing groups with the same name
702     *
703     * @param string Name to check
704     * @return string A group name which is unique for the current use
705     */
706    private function unique_groupname($name)
707    {
708        $checkname = $name;
709        $num = 2; $hit = false;
710
711        do {
712            $sql_result = $this->db->query(
713                "SELECT 1 FROM ".get_table_name($this->db_groups).
714                " WHERE del<>1".
715                    " AND user_id=?".
716                    " AND name=?",
717                $this->user_id,
718                $checkname);
719
720            // append number to make name unique
721            if ($hit = $this->db->num_rows($sql_result))
722                $checkname = $name . ' ' . $num++;
723        } while ($hit > 0);
724
725        return $checkname;
726    }
727
728}
Note: See TracBrowser for help on using the repository browser.