Aleksander Machniak
2016-04-10 d01c06eded2d2ff4c6c786469395906f92694703
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         $data = $this->get();
253         $data = array_reverse($data);
254         $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
255
256         $this->meta['pos'] = array();
257     }
258
259     /**
260      * Check if the given message ID exists in the object
261      *
262      * @param int  $msgid     Message ID
263      * @param bool $get_index When enabled element's index will be returned.
264      *                        Elements are indexed starting with 0
265      *
266      * @return mixed False if message ID doesn't exist, True if exists or
267      *               index of the element if $get_index=true
268      */
269     public function exists($msgid, $get_index = false)
270     {
271         if (empty($this->raw_data)) {
272             return false;
273         }
274
275         $msgid = (int) $msgid;
276         $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/')));
277         $end   = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/')));
278
279         if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
280             $get_index ? PREG_OFFSET_CAPTURE : null)
281         ) {
282             if ($get_index) {
283                 $idx = 0;
284                 if ($m[0][1]) {
285                     $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
286                 }
c321a9 287                 // cache position of this element, so we can use it in get_element()
40c45e 288                 $this->meta['pos'][$idx] = (int)$m[0][1];
A 289
290                 return $idx;
291             }
292             return true;
293         }
294
295         return false;
296     }
297
298     /**
299      * Return all messages in the result.
300      *
301      * @return array List of message IDs
302      */
303     public function get()
304     {
305         if (empty($this->raw_data)) {
306             return array();
307         }
3b1d41 308
40c45e 309         return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
A 310     }
311
312     /**
313      * Return all messages in the result.
314      *
315      * @return array List of message IDs
316      */
c321a9 317     public function get_compressed()
40c45e 318     {
A 319         if (empty($this->raw_data)) {
320             return '';
321         }
322
323         return rcube_imap_generic::compressMessageSet($this->get());
324     }
325
326     /**
327      * Return result element at specified index
328      *
329      * @param int|string  $index  Element's index or "FIRST" or "LAST"
330      *
331      * @return int Element value
332      */
c321a9 333     public function get_element($index)
40c45e 334     {
A 335         $count = $this->count();
336
337         if (!$count) {
338             return null;
339         }
340
341         // first element
342         if ($index === 0 || $index === '0' || $index === 'FIRST') {
343             $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
344             if ($pos === false)
345                 $result = (int) $this->raw_data;
346             else
347                 $result = (int) substr($this->raw_data, 0, $pos);
348
349             return $result;
350         }
351
352         // last element
353         if ($index === 'LAST' || $index == $count-1) {
354             $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
355             if ($pos === false)
356                 $result = (int) $this->raw_data;
357             else
358                 $result = (int) substr($this->raw_data, $pos);
359
360             return $result;
361         }
362
363         // do we know the position of the element or the neighbour of it?
364         if (!empty($this->meta['pos'])) {
365             if (isset($this->meta['pos'][$index]))
366                 $pos = $this->meta['pos'][$index];
367             else if (isset($this->meta['pos'][$index-1]))
368                 $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
369                     $this->meta['pos'][$index-1] + 1);
370             else if (isset($this->meta['pos'][$index+1]))
371                 $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
372                     $this->meta['pos'][$index+1] - $this->length() - 1);
373
374             if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) {
375                 return (int) $m[1];
376             }
377         }
378
379         // Finally use less effective method
380         $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
381
382         return $data[$index];
383     }
384
385     /**
386      * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
387      * or internal data e.g. MAILBOX, ORDER
388      *
389      * @param string $param  Parameter name
390      *
391      * @return array|string Response parameters or parameter value
392      */
c321a9 393     public function get_parameters($param=null)
40c45e 394     {
A 395         $params = $this->params;
396         $params['MAILBOX'] = $this->mailbox;
397         $params['ORDER']   = $this->order;
398
399         if ($param !== null) {
400             return $params[$param];
401         }
402
403         return $params;
404     }
405
406     /**
407      * Returns length of internal data representation
408      *
409      * @return int Data length
410      */
c321a9 411     protected function length()
40c45e 412     {
A 413         if (!isset($this->meta['length'])) {
414             $this->meta['length'] = strlen($this->raw_data);
415         }
416
417         return $this->meta['length'];
418     }
419 }