Aleksander Machniak
2016-04-10 d54eb6c95104316180bbaa777f2d95f8d88c0f3c
commit | author | age
40c45e 1 <?php
A 2
3 /*
4  +-----------------------------------------------------------------------+
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;
33     protected $meta = array();
34     protected $params = array();
35     protected $order = 'ASC';
40c45e 36
A 37     const SEPARATOR_ELEMENT = ' ';
38
39
40     /**
41      * Object constructor.
42      */
43     public function __construct($mailbox = null, $data = null)
44     {
45         $this->mailbox = $mailbox;
46         $this->init($data);
47     }
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     /**
132      * Checks the result from IMAP command
133      *
134      * @return bool True if the result is an error, False otherwise
135      */
c321a9 136     public function is_error()
40c45e 137     {
A 138         return $this->raw_data === null ? true : false;
139     }
140
141
142     /**
143      * Checks if the result is empty
144      *
145      * @return bool True if the result is empty, False otherwise
146      */
c321a9 147     public function is_empty()
40c45e 148     {
A 149         return empty($this->raw_data) ? true : false;
150     }
151
152
153     /**
154      * Returns number of elements in the result
155      *
156      * @return int Number of elements
157      */
158     public function count()
159     {
160         if ($this->meta['count'] !== null)
161             return $this->meta['count'];
162
163         if (empty($this->raw_data)) {
164             $this->meta['count']  = 0;
165             $this->meta['length'] = 0;
166         }
889665 167         else {
40c45e 168             $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
889665 169         }
40c45e 170
A 171         return $this->meta['count'];
172     }
173
174
175     /**
176      * Returns number of elements in the result.
177      * Alias for count() for compatibility with rcube_result_thread
178      *
179      * @return int Number of elements
180      */
c321a9 181     public function count_messages()
40c45e 182     {
A 183         return $this->count();
184     }
185
186
187     /**
188      * Returns maximal message identifier in the result
189      *
190      * @return int Maximal message identifier
191      */
192     public function max()
193     {
194         if (!isset($this->meta['max'])) {
195             $this->meta['max'] = (int) @max($this->get());
196         }
197
198         return $this->meta['max'];
199     }
200
201
202     /**
203      * Returns minimal message identifier in the result
204      *
205      * @return int Minimal message identifier
206      */
207     public function min()
208     {
209         if (!isset($this->meta['min'])) {
210             $this->meta['min'] = (int) @min($this->get());
211         }
212
213         return $this->meta['min'];
214     }
215
216
217     /**
218      * Slices data set.
219      *
220      * @param $offset Offset (as for PHP's array_slice())
221      * @param $length Number of elements (as for PHP's array_slice())
222      *
223      */
224     public function slice($offset, $length)
225     {
226         $data = $this->get();
227         $data = array_slice($data, $offset, $length);
228
229         $this->meta          = array();
230         $this->meta['count'] = count($data);
231         $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
232     }
233
234
235     /**
3b1d41 236      * Filters data set. Removes elements not listed in $ids list.
40c45e 237      *
A 238      * @param array $ids List of IDs to remove.
239      */
240     public function filter($ids = array())
241     {
242         $data = $this->get();
243         $data = array_intersect($data, $ids);
244
245         $this->meta          = array();
246         $this->meta['count'] = count($data);
247         $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
248     }
249
250
251     /**
252      * Reverts order of elements in the result
253      */
254     public function revert()
255     {
256         $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
257
258         if (empty($this->raw_data)) {
259             return;
260         }
261
262         $data = $this->get();
263         $data = array_reverse($data);
264         $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
265
266         $this->meta['pos'] = array();
267     }
268
269
270     /**
271      * Check if the given message ID exists in the object
272      *
273      * @param int  $msgid     Message ID
274      * @param bool $get_index When enabled element's index will be returned.
275      *                        Elements are indexed starting with 0
276      *
277      * @return mixed False if message ID doesn't exist, True if exists or
278      *               index of the element if $get_index=true
279      */
280     public function exists($msgid, $get_index = false)
281     {
282         if (empty($this->raw_data)) {
283             return false;
284         }
285
286         $msgid = (int) $msgid;
287         $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/')));
288         $end   = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/')));
289
290         if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
291             $get_index ? PREG_OFFSET_CAPTURE : null)
292         ) {
293             if ($get_index) {
294                 $idx = 0;
295                 if ($m[0][1]) {
296                     $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
297                 }
c321a9 298                 // cache position of this element, so we can use it in get_element()
40c45e 299                 $this->meta['pos'][$idx] = (int)$m[0][1];
A 300
301                 return $idx;
302             }
303             return true;
304         }
305
306         return false;
307     }
308
309
310     /**
311      * Return all messages in the result.
312      *
313      * @return array List of message IDs
314      */
315     public function get()
316     {
317         if (empty($this->raw_data)) {
318             return array();
319         }
3b1d41 320
40c45e 321         return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
A 322     }
323
324
325     /**
326      * Return all messages in the result.
327      *
328      * @return array List of message IDs
329      */
c321a9 330     public function get_compressed()
40c45e 331     {
A 332         if (empty($this->raw_data)) {
333             return '';
334         }
335
336         return rcube_imap_generic::compressMessageSet($this->get());
337     }
338
339
340     /**
341      * Return result element at specified index
342      *
343      * @param int|string  $index  Element's index or "FIRST" or "LAST"
344      *
345      * @return int Element value
346      */
c321a9 347     public function get_element($index)
40c45e 348     {
A 349         $count = $this->count();
350
351         if (!$count) {
352             return null;
353         }
354
355         // first element
356         if ($index === 0 || $index === '0' || $index === 'FIRST') {
357             $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
358             if ($pos === false)
359                 $result = (int) $this->raw_data;
360             else
361                 $result = (int) substr($this->raw_data, 0, $pos);
362
363             return $result;
364         }
365
366         // last element
367         if ($index === 'LAST' || $index == $count-1) {
368             $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
369             if ($pos === false)
370                 $result = (int) $this->raw_data;
371             else
372                 $result = (int) substr($this->raw_data, $pos);
373
374             return $result;
375         }
376
377         // do we know the position of the element or the neighbour of it?
378         if (!empty($this->meta['pos'])) {
379             if (isset($this->meta['pos'][$index]))
380                 $pos = $this->meta['pos'][$index];
381             else if (isset($this->meta['pos'][$index-1]))
382                 $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
383                     $this->meta['pos'][$index-1] + 1);
384             else if (isset($this->meta['pos'][$index+1]))
385                 $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
386                     $this->meta['pos'][$index+1] - $this->length() - 1);
387
388             if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) {
389                 return (int) $m[1];
390             }
391         }
392
393         // Finally use less effective method
394         $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
395
396         return $data[$index];
397     }
398
399
400     /**
401      * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
402      * or internal data e.g. MAILBOX, ORDER
403      *
404      * @param string $param  Parameter name
405      *
406      * @return array|string Response parameters or parameter value
407      */
c321a9 408     public function get_parameters($param=null)
40c45e 409     {
A 410         $params = $this->params;
411         $params['MAILBOX'] = $this->mailbox;
412         $params['ORDER']   = $this->order;
413
414         if ($param !== null) {
415             return $params[$param];
416         }
417
418         return $params;
419     }
420
421
422     /**
423      * Returns length of internal data representation
424      *
425      * @return int Data length
426      */
c321a9 427     protected function length()
40c45e 428     {
A 429         if (!isset($this->meta['length'])) {
430             $this->meta['length'] = strlen($this->raw_data);
431         }
432
433         return $this->meta['length'];
434     }
435 }