Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_result_thread.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4681f974d28b |
---|---|
1 <?php | |
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 | | |
8 | | | |
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. | | |
12 | | | |
13 | PURPOSE: | | |
14 | THREAD 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 THREAD result | |
23 * | |
24 * @package Framework | |
25 * @subpackage Storage | |
26 */ | |
27 class rcube_result_thread | |
28 { | |
29 public $incomplete = false; | |
30 | |
31 protected $raw_data; | |
32 protected $mailbox; | |
33 protected $meta = array(); | |
34 protected $order = 'ASC'; | |
35 | |
36 const SEPARATOR_ELEMENT = ' '; | |
37 const SEPARATOR_ITEM = '~'; | |
38 const SEPARATOR_LEVEL = ':'; | |
39 | |
40 | |
41 /** | |
42 * Object constructor. | |
43 */ | |
44 public function __construct($mailbox = null, $data = null) | |
45 { | |
46 $this->mailbox = $mailbox; | |
47 $this->init($data); | |
48 } | |
49 | |
50 /** | |
51 * Initializes object with IMAP 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 if (preg_match('/^ THREAD/i', $data[$i])) { | |
64 // valid response, initialize raw_data for is_error() | |
65 $this->raw_data = ''; | |
66 $data[$i] = substr($data[$i], 7); | |
67 break; | |
68 } | |
69 | |
70 unset($data[$i]); | |
71 } | |
72 | |
73 if (empty($data)) { | |
74 return; | |
75 } | |
76 | |
77 $data = array_shift($data); | |
78 $data = trim($data); | |
79 $data = preg_replace('/[\r\n]/', '', $data); | |
80 $data = preg_replace('/\s+/', ' ', $data); | |
81 | |
82 $this->raw_data = $this->parse_thread($data); | |
83 } | |
84 | |
85 /** | |
86 * Checks the result from IMAP command | |
87 * | |
88 * @return bool True if the result is an error, False otherwise | |
89 */ | |
90 public function is_error() | |
91 { | |
92 return $this->raw_data === null; | |
93 } | |
94 | |
95 /** | |
96 * Checks if the result is empty | |
97 * | |
98 * @return bool True if the result is empty, False otherwise | |
99 */ | |
100 public function is_empty() | |
101 { | |
102 return empty($this->raw_data); | |
103 } | |
104 | |
105 /** | |
106 * Returns number of elements (threads) in the result | |
107 * | |
108 * @return int Number of elements | |
109 */ | |
110 public function count() | |
111 { | |
112 if ($this->meta['count'] !== null) | |
113 return $this->meta['count']; | |
114 | |
115 if (empty($this->raw_data)) { | |
116 $this->meta['count'] = 0; | |
117 } | |
118 else { | |
119 $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT); | |
120 } | |
121 | |
122 if (!$this->meta['count']) | |
123 $this->meta['messages'] = 0; | |
124 | |
125 return $this->meta['count']; | |
126 } | |
127 | |
128 /** | |
129 * Returns number of all messages in the result | |
130 * | |
131 * @return int Number of elements | |
132 */ | |
133 public function count_messages() | |
134 { | |
135 if ($this->meta['messages'] !== null) | |
136 return $this->meta['messages']; | |
137 | |
138 if (empty($this->raw_data)) { | |
139 $this->meta['messages'] = 0; | |
140 } | |
141 else { | |
142 $this->meta['messages'] = 1 | |
143 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT) | |
144 + substr_count($this->raw_data, self::SEPARATOR_ITEM); | |
145 } | |
146 | |
147 if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1) | |
148 $this->meta['count'] = $this->meta['messages']; | |
149 | |
150 return $this->meta['messages']; | |
151 } | |
152 | |
153 /** | |
154 * Returns maximum message identifier in the result | |
155 * | |
156 * @return int Maximum message identifier | |
157 */ | |
158 public function max() | |
159 { | |
160 if (!isset($this->meta['max'])) { | |
161 $this->meta['max'] = (int) @max($this->get()); | |
162 } | |
163 return $this->meta['max']; | |
164 } | |
165 | |
166 /** | |
167 * Returns minimum message identifier in the result | |
168 * | |
169 * @return int Minimum message identifier | |
170 */ | |
171 public function min() | |
172 { | |
173 if (!isset($this->meta['min'])) { | |
174 $this->meta['min'] = (int) @min($this->get()); | |
175 } | |
176 return $this->meta['min']; | |
177 } | |
178 | |
179 /** | |
180 * Slices data set. | |
181 * | |
182 * @param $offset Offset (as for PHP's array_slice()) | |
183 * @param $length Number of elements (as for PHP's array_slice()) | |
184 */ | |
185 public function slice($offset, $length) | |
186 { | |
187 $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); | |
188 $data = array_slice($data, $offset, $length); | |
189 | |
190 $this->meta = array(); | |
191 $this->meta['count'] = count($data); | |
192 $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); | |
193 } | |
194 | |
195 /** | |
196 * Filters data set. Removes threads not listed in $roots list. | |
197 * | |
198 * @param array $roots List of IDs of thread roots. | |
199 */ | |
200 public function filter($roots) | |
201 { | |
202 $datalen = strlen($this->raw_data); | |
203 $roots = array_flip($roots); | |
204 $result = ''; | |
205 $start = 0; | |
206 | |
207 $this->meta = array(); | |
208 $this->meta['count'] = 0; | |
209 | |
210 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) | |
211 || ($start < $datalen && ($pos = $datalen)) | |
212 ) { | |
213 $len = $pos - $start; | |
214 $elem = substr($this->raw_data, $start, $len); | |
215 $start = $pos + 1; | |
216 | |
217 // extract root message ID | |
218 if ($npos = strpos($elem, self::SEPARATOR_ITEM)) { | |
219 $root = (int) substr($elem, 0, $npos); | |
220 } | |
221 else { | |
222 $root = $elem; | |
223 } | |
224 | |
225 if (isset($roots[$root])) { | |
226 $this->meta['count']++; | |
227 $result .= self::SEPARATOR_ELEMENT . $elem; | |
228 } | |
229 } | |
230 | |
231 $this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT); | |
232 } | |
233 | |
234 /** | |
235 * Reverts order of elements in the result | |
236 */ | |
237 public function revert() | |
238 { | |
239 $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; | |
240 | |
241 if (empty($this->raw_data)) { | |
242 return; | |
243 } | |
244 | |
245 $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); | |
246 $data = array_reverse($data); | |
247 $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); | |
248 | |
249 $this->meta['pos'] = array(); | |
250 } | |
251 | |
252 /** | |
253 * Check if the given message ID exists in the object | |
254 * | |
255 * @param int $msgid Message ID | |
256 * @param bool $get_index When enabled element's index will be returned. | |
257 * Elements are indexed starting with 0 | |
258 * | |
259 * @return boolean True on success, False if message ID doesn't exist | |
260 */ | |
261 public function exists($msgid, $get_index = false) | |
262 { | |
263 $msgid = (int) $msgid; | |
264 $begin = implode('|', array( | |
265 '^', | |
266 preg_quote(self::SEPARATOR_ELEMENT, '/'), | |
267 preg_quote(self::SEPARATOR_LEVEL, '/'), | |
268 )); | |
269 $end = implode('|', array( | |
270 '$', | |
271 preg_quote(self::SEPARATOR_ELEMENT, '/'), | |
272 preg_quote(self::SEPARATOR_ITEM, '/'), | |
273 )); | |
274 | |
275 if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m, | |
276 $get_index ? PREG_OFFSET_CAPTURE : null) | |
277 ) { | |
278 if ($get_index) { | |
279 $idx = 0; | |
280 if ($m[0][1]) { | |
281 $idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1) | |
282 + substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1); | |
283 } | |
284 // cache position of this element, so we can use it in get_element() | |
285 $this->meta['pos'][$idx] = (int)$m[0][1]; | |
286 | |
287 return $idx; | |
288 } | |
289 return true; | |
290 } | |
291 | |
292 return false; | |
293 } | |
294 | |
295 /** | |
296 * Return IDs of all messages in the result. Threaded data will be flattened. | |
297 * | |
298 * @return array List of message identifiers | |
299 */ | |
300 public function get() | |
301 { | |
302 if (empty($this->raw_data)) { | |
303 return array(); | |
304 } | |
305 | |
306 $regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/') | |
307 . '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') | |
308 .')/'; | |
309 | |
310 return preg_split($regexp, $this->raw_data); | |
311 } | |
312 | |
313 /** | |
314 * Return all messages in the result. | |
315 * | |
316 * @return array List of message identifiers | |
317 */ | |
318 public function get_compressed() | |
319 { | |
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 (all messages, not roots) | |
329 * | |
330 * @param int|string $index Element's index or "FIRST" or "LAST" | |
331 * | |
332 * @return int Element value | |
333 */ | |
334 public function get_element($index) | |
335 { | |
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 preg_match('/^([0-9]+)/', $this->raw_data, $m); | |
345 $result = (int) $m[1]; | |
346 return $result; | |
347 } | |
348 | |
349 // last element | |
350 if ($index === 'LAST' || $index == $count-1) { | |
351 preg_match('/([0-9]+)$/', $this->raw_data, $m); | |
352 $result = (int) $m[1]; | |
353 return $result; | |
354 } | |
355 | |
356 // do we know the position of the element or the neighbour of it? | |
357 if (!empty($this->meta['pos'])) { | |
358 $element = preg_quote(self::SEPARATOR_ELEMENT, '/'); | |
359 $item = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?'; | |
360 $regexp = '(' . $element . '|' . $item . ')'; | |
361 | |
362 if (isset($this->meta['pos'][$index])) { | |
363 if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index])) | |
364 $result = $m[1]; | |
365 } | |
366 else if (isset($this->meta['pos'][$index-1])) { | |
367 // get chunk of data after previous element | |
368 $data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50); | |
369 $data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position | |
370 $data = preg_replace("/^$regexp/", '', $data); // remove separator | |
371 if (preg_match('/^([0-9]+)/', $data, $m)) | |
372 $result = $m[1]; | |
373 } | |
374 else if (isset($this->meta['pos'][$index+1])) { | |
375 // get chunk of data before next element | |
376 $pos = max(0, $this->meta['pos'][$index+1] - 50); | |
377 $len = min(50, $this->meta['pos'][$index+1]); | |
378 $data = substr($this->raw_data, $pos, $len); | |
379 $data = preg_replace("/$regexp\$/", '', $data); // remove separator | |
380 | |
381 if (preg_match('/([0-9]+)$/', $data, $m)) | |
382 $result = $m[1]; | |
383 } | |
384 | |
385 if (isset($result)) { | |
386 return (int) $result; | |
387 } | |
388 } | |
389 | |
390 // Finally use less effective method | |
391 $data = $this->get(); | |
392 | |
393 return $data[$index]; | |
394 } | |
395 | |
396 /** | |
397 * Returns response parameters e.g. MAILBOX, ORDER | |
398 * | |
399 * @param string $param Parameter name | |
400 * | |
401 * @return array|string Response parameters or parameter value | |
402 */ | |
403 public function get_parameters($param=null) | |
404 { | |
405 $params = array(); | |
406 $params['MAILBOX'] = $this->mailbox; | |
407 $params['ORDER'] = $this->order; | |
408 | |
409 if ($param !== null) { | |
410 return $params[$param]; | |
411 } | |
412 | |
413 return $params; | |
414 } | |
415 | |
416 /** | |
417 * THREAD=REFS sorting implementation (based on provided index) | |
418 * | |
419 * @param rcube_result_index $index Sorted message identifiers | |
420 */ | |
421 public function sort($index) | |
422 { | |
423 $this->sort_order = $index->get_parameters('ORDER'); | |
424 | |
425 if (empty($this->raw_data)) { | |
426 return; | |
427 } | |
428 | |
429 // when sorting search result it's good to make the index smaller | |
430 if ($index->count() != $this->count_messages()) { | |
431 $index->filter($this->get()); | |
432 } | |
433 | |
434 $result = array_fill_keys($index->get(), null); | |
435 $datalen = strlen($this->raw_data); | |
436 $start = 0; | |
437 | |
438 // Here we're parsing raw_data twice, we want only one big array | |
439 // in memory at a time | |
440 | |
441 // Assign roots | |
442 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) | |
443 || ($start < $datalen && ($pos = $datalen)) | |
444 ) { | |
445 $len = $pos - $start; | |
446 $elem = substr($this->raw_data, $start, $len); | |
447 $start = $pos + 1; | |
448 | |
449 $items = explode(self::SEPARATOR_ITEM, $elem); | |
450 $root = (int) array_shift($items); | |
451 | |
452 if ($root) { | |
453 $result[$root] = $root; | |
454 foreach ($items as $item) { | |
455 list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item); | |
456 $result[$id] = $root; | |
457 } | |
458 } | |
459 } | |
460 | |
461 // get only unique roots | |
462 $result = array_filter($result); // make sure there are no nulls | |
463 $result = array_unique($result); | |
464 | |
465 // Re-sort raw data | |
466 $result = array_fill_keys($result, null); | |
467 $start = 0; | |
468 | |
469 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) | |
470 || ($start < $datalen && ($pos = $datalen)) | |
471 ) { | |
472 $len = $pos - $start; | |
473 $elem = substr($this->raw_data, $start, $len); | |
474 $start = $pos + 1; | |
475 | |
476 $npos = strpos($elem, self::SEPARATOR_ITEM); | |
477 $root = (int) ($npos ? substr($elem, 0, $npos) : $elem); | |
478 | |
479 $result[$root] = $elem; | |
480 } | |
481 | |
482 $this->raw_data = implode(self::SEPARATOR_ELEMENT, $result); | |
483 } | |
484 | |
485 /** | |
486 * Returns data as tree | |
487 * | |
488 * @return array Data tree | |
489 */ | |
490 public function get_tree() | |
491 { | |
492 $datalen = strlen($this->raw_data); | |
493 $result = array(); | |
494 $start = 0; | |
495 | |
496 while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) | |
497 || ($start < $datalen && ($pos = $datalen)) | |
498 ) { | |
499 $len = $pos - $start; | |
500 $elem = substr($this->raw_data, $start, $len); | |
501 $items = explode(self::SEPARATOR_ITEM, $elem); | |
502 $result[array_shift($items)] = $this->build_thread($items); | |
503 $start = $pos + 1; | |
504 } | |
505 | |
506 return $result; | |
507 } | |
508 | |
509 /** | |
510 * Returns thread depth and children data | |
511 * | |
512 * @return array Thread data | |
513 */ | |
514 public function get_thread_data() | |
515 { | |
516 $data = $this->get_tree(); | |
517 $depth = array(); | |
518 $children = array(); | |
519 | |
520 $this->build_thread_data($data, $depth, $children); | |
521 | |
522 return array($depth, $children); | |
523 } | |
524 | |
525 /** | |
526 * Creates 'depth' and 'children' arrays from stored thread 'tree' data. | |
527 */ | |
528 protected function build_thread_data($data, &$depth, &$children, $level = 0) | |
529 { | |
530 foreach ((array)$data as $key => $val) { | |
531 $empty = empty($val) || !is_array($val); | |
532 $children[$key] = !$empty; | |
533 $depth[$key] = $level; | |
534 if (!$empty) { | |
535 $this->build_thread_data($val, $depth, $children, $level + 1); | |
536 } | |
537 } | |
538 } | |
539 | |
540 /** | |
541 * Converts part of the raw thread into an array | |
542 */ | |
543 protected function build_thread($items, $level = 1, &$pos = 0) | |
544 { | |
545 $result = array(); | |
546 | |
547 for ($len=count($items); $pos < $len; $pos++) { | |
548 list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]); | |
549 if ($level == $lv) { | |
550 $pos++; | |
551 $result[$id] = $this->build_thread($items, $level+1, $pos); | |
552 } | |
553 else { | |
554 $pos--; | |
555 break; | |
556 } | |
557 } | |
558 | |
559 return $result; | |
560 } | |
561 | |
562 /** | |
563 * IMAP THREAD response parser | |
564 */ | |
565 protected function parse_thread($str, $begin = 0, $end = 0, $depth = 0) | |
566 { | |
567 // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about | |
568 // 7 times instead :-) See comments on http://uk2.php.net/references and this article: | |
569 // http://derickrethans.nl/files/phparch-php-variables-article.pdf | |
570 $node = ''; | |
571 if (!$end) { | |
572 $end = strlen($str); | |
573 } | |
574 | |
575 // Let's try to store data in max. compacted stracture as a string, | |
576 // arrays handling is much more expensive | |
577 // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96)) | |
578 // -- 2 | |
579 // -- 3 | |
580 // \-- 6 | |
581 // |-- 4 | |
582 // | \-- 23 | |
583 // | | |
584 // \-- 44 | |
585 // \-- 7 | |
586 // \-- 96 | |
587 // | |
588 // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96 | |
589 | |
590 if ($str[$begin] != '(') { | |
591 // find next bracket | |
592 $stop = $begin + strcspn($str, '()', $begin, $end - $begin); | |
593 $messages = explode(' ', trim(substr($str, $begin, $stop - $begin))); | |
594 | |
595 if (empty($messages)) { | |
596 return $node; | |
597 } | |
598 | |
599 foreach ($messages as $msg) { | |
600 if ($msg) { | |
601 $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg; | |
602 $this->meta['messages']++; | |
603 $depth++; | |
604 } | |
605 } | |
606 | |
607 if ($stop < $end) { | |
608 $node .= $this->parse_thread($str, $stop, $end, $depth); | |
609 } | |
610 } | |
611 else { | |
612 $off = $begin; | |
613 while ($off < $end) { | |
614 $start = $off; | |
615 $off++; | |
616 $n = 1; | |
617 while ($n > 0) { | |
618 $p = strpos($str, ')', $off); | |
619 if ($p === false) { | |
620 // error, wrong structure, mismatched brackets in IMAP THREAD response | |
621 // @TODO: write error to the log or maybe set $this->raw_data = null; | |
622 return $node; | |
623 } | |
624 $p1 = strpos($str, '(', $off); | |
625 if ($p1 !== false && $p1 < $p) { | |
626 $off = $p1 + 1; | |
627 $n++; | |
628 } | |
629 else { | |
630 $off = $p + 1; | |
631 $n--; | |
632 } | |
633 } | |
634 | |
635 $thread = $this->parse_thread($str, $start + 1, $off - 1, $depth); | |
636 if ($thread) { | |
637 if (!$depth) { | |
638 if ($node) { | |
639 $node .= self::SEPARATOR_ELEMENT; | |
640 } | |
641 } | |
642 $node .= $thread; | |
643 } | |
644 } | |
645 } | |
646 | |
647 return $node; | |
648 } | |
649 } |