1<?php
2/**
3 * The Turba_List:: class provides an interface for dealing with a
4 * list of Turba_Objects.
5 *
6 * Copyright 2000-2017 Horde LLC (http://www.horde.org/)
7 *
8 * See the enclosed file LICENSE for license information (ASL).  If you did
9 * did not receive this file, see http://www.horde.org/licenses/apache.
10 *
11 * @author   Chuck Hagenbuch <chuck@horde.org>
12 * @author   Jon Parise <jon@csh.rit.edu>
13 * @category Horde
14 * @license  http://www.horde.org/licenses/apache ASL
15 * @package  Turba
16 */
17class Turba_List implements Countable
18{
19    /**
20     * The array containing the Turba_Objects represented in this list.
21     *
22     * @var array
23     */
24    public $objects = array();
25
26    /**
27     * The field to compare objects by.
28     *
29     * @var string
30     */
31    protected $_usortCriteria;
32
33    /**
34     * Constructor.
35     */
36    public function __construct(array $ids = array())
37    {
38        foreach ($ids as $value) {
39            list($source, $key) = explode(':', $value);
40            try {
41                $driver = $GLOBALS['injector']->getInstance('Turba_Factory_Driver')->create($source);
42                $this->insert($driver->getObject($key));
43            } catch (Horde_Exception $e) {
44            }
45        }
46    }
47
48    /**
49     * Inserts a new object into the list.
50     *
51     * @param Turba_Object $object  The object to insert.
52     */
53    public function insert(Turba_Object $object)
54    {
55        if ($object instanceof Turba_Object) {
56            $key = $object->getSource() . ':' . $object->getValue('__key');
57            if (!isset($this->objects[$key])) {
58                $this->objects[$key] = $object;
59            }
60        }
61    }
62
63    /**
64     * Resets our internal pointer to the beginning of the list. Use this to
65     * hide the internal storage (array, list, etc.) from client objects.
66     *
67     * @return Turba_Object  The next object in the list.
68     */
69    public function reset()
70    {
71        return reset($this->objects);
72    }
73
74    /**
75     * Returns the next Turba_Object in the list. Use this to hide internal
76     * implementation details from client objects.
77     *
78     * @return Turba_Object  The next object in the list.
79     */
80    public function next()
81    {
82        list(,$tmp) = each($this->objects);
83        return $tmp;
84    }
85
86    /**
87     * Filters/Sorts the list based on the specified sort routine.
88     * The default sort order is by last name, ascending.
89     *
90     * @param array $order  Array of hashes describing sort fields.  Each
91     *                      hash has the following fields:
92     * <pre>
93     * ascending - (boolean) Sort direction.
94     * field - (string) Sort field.
95     * </pre>
96     */
97    public function sort($order = null)
98    {
99        global $attributes, $prefs;
100
101        if (!$order) {
102            $order = array(
103                array(
104                    'ascending' => true,
105                    'field' => 'lastname'
106                )
107            );
108        }
109
110        $need_lastname = $need_firstname = false;
111        $name_format = $prefs->getValue('name_format');
112        $name_sort = $prefs->getValue('name_sort');
113        foreach ($order as &$field) {
114            if ($field['field'] == 'name') {
115                if ($name_sort == 'last_first') {
116                    $field['field'] = 'lastname';
117                } elseif ($name_sort == 'first_last') {
118                    $field['field'] = 'firstname';
119                }
120            }
121
122            if ($field['field'] == 'lastname') {
123                $field['field'] = '__lastname';
124                $need_lastname = true;
125                break;
126            }
127            if ($field['field'] == 'firstname') {
128                $field['field'] = '__firstname';
129                $need_firstname = true;
130                break;
131            }
132        }
133
134        if ($need_firstname || $need_lastname) {
135            $sorted_objects = array();
136            foreach ($this->objects as $key => $object) {
137                $name = $object->getValue('name');
138                $firstname = $object->getValue('firstname');
139                $lastname = $object->getValue('lastname');
140                if (!$lastname) {
141                    $lastname = Turba::guessLastname($name);
142                }
143                if (!$firstname) {
144                    switch ($name_format) {
145                    case 'last_first':
146                        $firstname = preg_replace('/' . preg_quote($lastname, '/') . ',\s*/', '', $name);
147                        break;
148                    case 'first_last':
149                        $firstname = preg_replace('/\s+' . preg_quote($lastname, '/') . '/', '', $name);
150                        break;
151                    default:
152                        $firstname = preg_replace('/\s*' . preg_quote($lastname, '/') . '(,\s*)?/', '', $name);
153                        break;
154                    }
155                }
156                $object->setValue('__lastname', $lastname);
157                $object->setValue('__firstname', $firstname);
158                $sorted_objects[$key] = $object;
159            }
160        } else {
161            $sorted_objects = $this->objects;
162        }
163
164        // Set the comparison type based on the type of attribute we're
165        // sorting by.
166        foreach ($order as &$val) {
167            $sm = 'text';
168
169            if (isset($attributes[$val['field']])) {
170                $f = $attributes[$val['field']];
171                if (!empty($f['cmptype'])) {
172                    $sm = $f['cmptype'];
173                } elseif (in_array($f['type'], array('int', 'intlist', 'number'))) {
174                    $sm = 'int';
175                }
176            }
177
178            $val['sortmethod'] = $sm;
179        }
180        $this->_usortCriteria = $order;
181
182        /* Exceptions thrown inside a sort incorrectly cause an error. See
183         * Bug #9202. */
184        @usort($sorted_objects, array($this, '_cmp'));
185
186        $this->objects = $sorted_objects;
187    }
188
189    /**
190     * Usort helper function.
191     *
192     * Compares two Turba_Objects based on the member variable
193     * $_usortCriteria, taking care to sort numerically if it is an integer
194     * field.
195     *
196     * @param Turba_Object $a  The first Turba_Object to compare.
197     * @param Turba_Object $b  The second Turba_Object to compare.
198     *
199     * @return integer  Comparison of the two field values.
200     */
201    protected function _cmp(Turba_Object $a, Turba_Object $b)
202    {
203        foreach ($this->_usortCriteria as $field) {
204            $f = $field['field'];
205
206            switch ($field['sortmethod']) {
207            case 'int':
208                $result = ($a->getValue($f) > $b->getValue($f)) ? 1 : -1;
209                break;
210
211            case 'text':
212                if (!isset($a->sortValue[$f])) {
213                    $a->sortValue[$f] = Horde_String::lower($a->getValue($f), true, 'UTF-8');
214                }
215                if (!isset($b->sortValue[$f])) {
216                    $b->sortValue[$f] = Horde_String::lower($b->getValue($f), true, 'UTF-8');
217                }
218
219                // Use strcoll for locale-safe comparisons.
220                $result = strcoll($a->sortValue[$f], $b->sortValue[$f]);
221                break;
222            }
223
224            if ($result != 0) {
225                return (($field['ascending'] ? 1 : -1) * $result);
226            }
227        }
228
229        return 0;
230    }
231
232    /* Countable methods. */
233
234    public function count()
235    {
236        return count($this->objects);
237    }
238
239}
240