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