source: subversion/trunk/roundcubemail/program/include/rcube_result_index.php @ 5561

Last change on this file since 5561 was 5561, checked in by alec, 18 months ago
  • Cleanup + perf. improvement (substr_count() is really fast!)
File size: 12.4 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_result_index.php                                |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
9 | Copyright (C) 2011, Kolab Systems AG                                  |
10 | Licensed under the GNU GPL                                            |
11 |                                                                       |
12 | PURPOSE:                                                              |
13 |   SORT/SEARCH/ESEARCH response handler                                |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 | Author: Aleksander Machniak <alec@alec.pl>                            |
18 +-----------------------------------------------------------------------+
19
20 $Id: rcube_imap.php 5347 2011-10-19 06:35:29Z alec $
21
22*/
23
24
25/**
26 * Class for accessing IMAP's SORT/SEARCH/ESEARCH result
27 */
28class rcube_result_index
29{
30    private $raw_data;
31    private $mailbox;
32    private $meta = array();
33    private $params = array();
34    private $order = 'ASC';
35
36    const SEPARATOR_ELEMENT = ' ';
37
38
39    /**
40     * Object constructor.
41     */
42    public function __construct($mailbox = null, $data = null)
43    {
44        $this->mailbox = $mailbox;
45        $this->init($data);
46    }
47
48
49    /**
50     * Initializes object with SORT command response
51     *
52     * @param string $data IMAP response string
53     */
54    public function init($data = null)
55    {
56        $this->meta = array();
57
58        $data = explode('*', (string)$data);
59
60        // ...skip unilateral untagged server responses
61        for ($i=0, $len=count($data); $i<$len; $i++) {
62            $data_item = &$data[$i];
63            if (preg_match('/^ SORT/i', $data_item)) {
64                $data_item = substr($data_item, 5);
65                break;
66            }
67            else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) {
68                $data_item = substr($data_item, strlen($m[0]));
69
70                if (strtoupper($m[1]) == 'ESEARCH') {
71                    $data_item = trim($data_item);
72                    // remove MODSEQ response
73                    if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) {
74                        $data_item = substr($data_item, 0, -strlen($m[0]));
75                        $this->params['MODSEQ'] = $m[1];
76                    }
77                    // remove TAG response part
78                    if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) {
79                        $data_item = substr($data_item, strlen($m[0]));
80                    }
81                    // remove UID
82                    $data_item = preg_replace('/^UID\s*/i', '', $data_item);
83
84                    // ESEARCH parameters
85                    while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) {
86                        $param = strtoupper($m[1]);
87                        $value = $m[2];
88
89                        $this->params[strtoupper($m[1])] = $value;
90                        $data_item = substr($data_item, strlen($m[0]));
91
92                        if (in_array($param, array('COUNT', 'MIN', 'MAX'))) {
93                            $this->meta[strtolower($param)] = (int) $m[2];
94                        }
95                    }
96
97// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ?
98// @TODO: work with compressed result?!
99                    if (isset($this->params['ALL'])) {
100                        $data[$idx] = implode(self::SEPARATOR_ELEMENT,
101                            rcube_imap_generic::uncompressMessageSet($this->params['ALL']));
102                    }
103                }
104
105                break;
106            }
107
108            unset($data[$i]);
109        }
110
111        if (empty($data)) {
112            return;
113        }
114
115        $data = array_shift($data);
116        $data = trim($data);
117        $data = preg_replace('/[\r\n]/', '', $data);
118        $data = preg_replace('/\s+/', ' ', $data);
119
120        $this->raw_data = $data;
121    }
122
123
124    /**
125     * Checks the result from IMAP command
126     *
127     * @return bool True if the result is an error, False otherwise
128     */
129    public function isError()
130    {
131        return $this->raw_data === null ? true : false;
132    }
133
134
135    /**
136     * Checks if the result is empty
137     *
138     * @return bool True if the result is empty, False otherwise
139     */
140    public function isEmpty()
141    {
142        return empty($this->raw_data) ? true : false;
143    }
144
145
146    /**
147     * Returns number of elements in the result
148     *
149     * @return int Number of elements
150     */
151    public function count()
152    {
153        if ($this->meta['count'] !== null)
154            return $this->meta['count'];
155
156        if (empty($this->raw_data)) {
157            $this->meta['count']  = 0;
158            $this->meta['length'] = 0;
159        }
160        else {
161            $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
162        }
163
164        return $this->meta['count'];
165    }
166
167
168    /**
169     * Returns number of elements in the result.
170     * Alias for count() for compatibility with rcube_result_thread
171     *
172     * @return int Number of elements
173     */
174    public function countMessages()
175    {
176        return $this->count();
177    }
178
179
180    /**
181     * Returns maximal message identifier in the result
182     *
183     * @return int Maximal message identifier
184     */
185    public function max()
186    {
187        if (!isset($this->meta['max'])) {
188            $this->meta['max'] = (int) @max($this->get());
189        }
190
191        return $this->meta['max'];
192    }
193
194
195    /**
196     * Returns minimal message identifier in the result
197     *
198     * @return int Minimal message identifier
199     */
200    public function min()
201    {
202        if (!isset($this->meta['min'])) {
203            $this->meta['min'] = (int) @min($this->get());
204        }
205
206        return $this->meta['min'];
207    }
208
209
210    /**
211     * Slices data set.
212     *
213     * @param $offset Offset (as for PHP's array_slice())
214     * @param $length Number of elements (as for PHP's array_slice())
215     *
216     */
217    public function slice($offset, $length)
218    {
219        $data = $this->get();
220        $data = array_slice($data, $offset, $length);
221
222        $this->meta          = array();
223        $this->meta['count'] = count($data);
224        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
225    }
226
227
228    /**
229     * Filters data set. Removes elements listed in $ids list.
230     *
231     * @param array $ids List of IDs to remove.
232     */
233    public function filter($ids = array())
234    {
235        $data = $this->get();
236        $data = array_diff($data, $ids);
237
238        $this->meta          = array();
239        $this->meta['count'] = count($data);
240        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
241    }
242
243
244    /**
245     * Filters data set. Removes elements not listed in $ids list.
246     *
247     * @param array $ids List of IDs to keep.
248     */
249    public function intersect($ids = array())
250    {
251        $data = $this->get();
252        $data = array_intersect($data, $ids);
253
254        $this->meta          = array();
255        $this->meta['count'] = count($data);
256        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
257    }
258
259
260    /**
261     * Reverts order of elements in the result
262     */
263    public function revert()
264    {
265        $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
266
267        if (empty($this->raw_data)) {
268            return;
269        }
270
271        // @TODO: maybe do this in chunks
272        $data = $this->get();
273        $data = array_reverse($data);
274        $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
275
276        $this->meta['pos'] = array();
277    }
278
279
280    /**
281     * Check if the given message ID exists in the object
282     *
283     * @param int  $msgid     Message ID
284     * @param bool $get_index When enabled element's index will be returned.
285     *                        Elements are indexed starting with 0
286     *
287     * @return mixed False if message ID doesn't exist, True if exists or
288     *               index of the element if $get_index=true
289     */
290    public function exists($msgid, $get_index = false)
291    {
292        if (empty($this->raw_data)) {
293            return false;
294        }
295
296        $msgid = (int) $msgid;
297        $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/')));
298        $end   = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/')));
299
300        if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
301            $get_index ? PREG_OFFSET_CAPTURE : null)
302        ) {
303            if ($get_index) {
304                $idx = 0;
305                if ($m[0][1]) {
306                    $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
307                }
308                // cache position of this element, so we can use it in getElement()
309                $this->meta['pos'][$idx] = (int)$m[0][1];
310
311                return $idx;
312            }
313            return true;
314        }
315
316        return false;
317    }
318
319
320    /**
321     * Return all messages in the result.
322     *
323     * @return array List of message IDs
324     */
325    public function get()
326    {
327        if (empty($this->raw_data)) {
328            return array();
329        }
330        return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
331    }
332
333
334    /**
335     * Return all messages in the result.
336     *
337     * @return array List of message IDs
338     */
339    public function getCompressed()
340    {
341        if (empty($this->raw_data)) {
342            return '';
343        }
344
345        return rcube_imap_generic::compressMessageSet($this->get());
346    }
347
348
349    /**
350     * Return result element at specified index
351     *
352     * @param int|string  $index  Element's index or "FIRST" or "LAST"
353     *
354     * @return int Element value
355     */
356    public function getElement($index)
357    {
358        $count = $this->count();
359
360        if (!$count) {
361            return null;
362        }
363
364        // first element
365        if ($index === 0 || $index === '0' || $index === 'FIRST') {
366            $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
367            if ($pos === false)
368                $result = (int) $this->raw_data;
369            else
370                $result = (int) substr($this->raw_data, 0, $pos);
371
372            return $result;
373        }
374
375        // last element
376        if ($index === 'LAST' || $index == $count-1) {
377            $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
378            if ($pos === false)
379                $result = (int) $this->raw_data;
380            else
381                $result = (int) substr($this->raw_data, $pos);
382
383            return $result;
384        }
385
386        // do we know the position of the element or the neighbour of it?
387        if (!empty($this->meta['pos'])) {
388            if (isset($this->meta['pos'][$index]))
389                $pos = $this->meta['pos'][$index];
390            else if (isset($this->meta['pos'][$index-1]))
391                $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
392                    $this->meta['pos'][$index-1] + 1);
393            else if (isset($this->meta['pos'][$index+1]))
394                $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
395                    $this->meta['pos'][$index+1] - $this->length() - 1);
396
397            if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) {
398                return (int) $m[1];
399            }
400        }
401
402        // Finally use less effective method
403        $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
404
405        return $data[$index];
406    }
407
408
409    /**
410     * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
411     * or internal data e.g. MAILBOX, ORDER
412     *
413     * @param string $param  Parameter name
414     *
415     * @return array|string Response parameters or parameter value
416     */
417    public function getParameters($param=null)
418    {
419        $params = $this->params;
420        $params['MAILBOX'] = $this->mailbox;
421        $params['ORDER']   = $this->order;
422
423        if ($param !== null) {
424            return $params[$param];
425        }
426
427        return $params;
428    }
429
430
431    /**
432     * Returns length of internal data representation
433     *
434     * @return int Data length
435     */
436    private function length()
437    {
438        if (!isset($this->meta['length'])) {
439            $this->meta['length'] = strlen($this->raw_data);
440        }
441
442        return $this->meta['length'];
443    }
444}
Note: See TracBrowser for help on using the repository browser.