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