1<?php 2/** 3 * Copyright 2000-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file COPYING for license information (GPL). If you 6 * did not receive this file, see http://www.horde.org/licenses/gpl. 7 * 8 * @category Horde 9 * @copyright 2000-2017 Horde LLC 10 * @license http://www.horde.org/licenses/gpl GPL 11 * @package IMP 12 */ 13 14/** 15 * IMP_Ftree (folder tree) provides a tree view of the mailboxes on a backend 16 * (a/k/a a folder list; in IMP, folders = collection of mailboxes), along 17 * with other display elements (Remote Accounts; Virtual Folders). 18 * 19 * @author Chuck Hagenbuch <chuck@horde.org> 20 * @author Anil Madhavapeddy <avsm@horde.org> 21 * @author Jon Parise <jon@horde.org> 22 * @author Michael Slusarz <slusarz@horde.org> 23 * @category Horde 24 * @copyright 2000-2017 Horde LLC 25 * @license http://www.horde.org/licenses/gpl GPL 26 * @package IMP 27 * 28 * @property-read boolean $changed Has the tree changed? 29 * @property-read IMP_Ftree_Eltdiff $eltdiff Element diff tracker. 30 * @property-read IMP_FTree_Prefs_Expanded $expanded The expanded folders 31 * list. 32 * @property-read IMP_Ftree_Prefs_Poll $poll The poll list. 33 * @property-read boolean $subscriptions Whether IMAP subscriptions are 34 * enabled. 35 * @property-read boolean $unsubscribed_loaded True if unsubscribed mailboxes 36 * have been loaded. 37 */ 38class IMP_Ftree 39implements ArrayAccess, Countable, IteratorAggregate, Serializable 40{ 41 /* Constants for mailboxElt attributes. */ 42 const ELT_NOSELECT = 1; 43 const ELT_NAMESPACE_OTHER = 2; 44 const ELT_NAMESPACE_SHARED = 4; 45 const ELT_IS_OPEN = 8; 46 const ELT_IS_SUBSCRIBED = 16; 47 const ELT_NOINFERIORS = 32; 48 const ELT_IS_POLLED = 64; 49 const ELT_NOT_POLLED = 128; 50 const ELT_VFOLDER = 256; 51 const ELT_NONIMAP = 512; 52 const ELT_INVISIBLE = 1024; 53 const ELT_NEED_SORT = 2048; 54 const ELT_REMOTE = 4096; 55 const ELT_REMOTE_AUTH = 8192; 56 const ELT_REMOTE_MBOX = 16384; 57 58 /* The string used to indicate the base of the tree. This must include 59 * null since this is the only 7-bit character not allowed in IMAP 60 * mailboxes (nulls allow us to sort by name but never conflict with an 61 * IMAP mailbox). */ 62 const BASE_ELT = "base\0"; 63 64 /** 65 * Account sources. 66 * 67 * @var array 68 */ 69 protected $_accounts; 70 71 /** 72 * Tree changed flag. Set when something in the tree has been altered. 73 * 74 * @var boolean 75 */ 76 protected $_changed = false; 77 78 /** 79 * Element diff tracking. 80 * 81 * @var IMP_Ftree_Eltdiff 82 */ 83 protected $_eltdiff; 84 85 /** 86 * Array containing the mailbox elements. 87 * 88 * @var array 89 */ 90 protected $_elts; 91 92 /** 93 * Parent/child list. 94 * 95 * @var array 96 */ 97 protected $_parent; 98 99 /** 100 * Temporary data that is not saved across serialization. 101 * 102 * @var array 103 */ 104 protected $_temp = array(); 105 106 /** 107 * Constructor. 108 */ 109 public function __construct() 110 { 111 $this->init(); 112 } 113 114 /** 115 */ 116 public function __get($name) 117 { 118 global $prefs; 119 120 switch ($name) { 121 case 'changed': 122 return ($this->_changed || $this->eltdiff->changed); 123 124 case 'expanded': 125 if (!isset($this->_temp['expanded'])) { 126 $this->_temp['expanded'] = new IMP_Ftree_Prefs_Expanded(); 127 } 128 return $this->_temp['expanded']; 129 130 case 'eltdiff': 131 return $this->_eltdiff; 132 133 case 'poll': 134 if (!isset($this->_temp['poll'])) { 135 $this->_temp['poll'] = new IMP_Ftree_Prefs_Poll($this); 136 } 137 return $this->_temp['poll']; 138 139 case 'subscriptions': 140 return $prefs->getValue('subscribe'); 141 142 case 'unsubscribed_loaded': 143 return $this[self::BASE_ELT]->subscribed; 144 } 145 } 146 147 /** 148 * Initialize the tree. 149 */ 150 public function init() 151 { 152 global $injector, $session; 153 154 $access_folders = $injector->getInstance('IMP_Factory_Imap')->create()->access(IMP_Imap::ACCESS_FOLDERS); 155 156 /* Reset class variables to the defaults. */ 157 $this->_accounts = $this->_elts = $this->_parent = array(); 158 $this->_changed = true; 159 160 $old_track = (isset($this->_eltdiff) && $this->_eltdiff->track); 161 $this->_eltdiff = new IMP_Ftree_Eltdiff(); 162 163 /* Create a placeholder element to the base of the tree so we can 164 * keep track of whether the base level needs to be sorted. */ 165 $this->_elts[self::BASE_ELT] = self::ELT_NEED_SORT | self::ELT_NONIMAP; 166 $this->_parent[self::BASE_ELT] = array(); 167 168 $mask = IMP_Ftree_Account::INIT; 169 if (!$access_folders || !$this->subscriptions || $session->get('imp', 'showunsub')) { 170 $mask |= IMP_Ftree_Account::UNSUB; 171 $this->setAttribute('subscribed', self::BASE_ELT, true); 172 } 173 174 /* Add base account. */ 175 $ob = $this->_accounts[self::BASE_ELT] = $access_folders 176 ? new IMP_Ftree_Account_Imap() 177 : new IMP_Ftree_Account_Inboxonly(); 178 array_map(array($this, '_insertElt'), $ob->getList(null, $mask)); 179 180 if ($access_folders) { 181 /* Add remote servers. */ 182 $this->insert(iterator_to_array( 183 $injector->getInstance('IMP_Remote') 184 )); 185 186 /* Add virtual folders to the tree. */ 187 $this->insert(iterator_to_array( 188 IMP_Search_IteratorFilter::create( 189 IMP_Search_IteratorFilter::VFOLDER 190 ) 191 )); 192 } 193 194 if ($old_track) { 195 $this->eltdiff->track = true; 196 } 197 } 198 199 /** 200 * Insert an element into the tree. 201 * 202 * @param mixed $id The name of the mailbox (or a list of mailboxes), 203 * an IMP_Search_Vfolder object, an IMP_Remote_Account 204 * object, or an array containing any mixture of these. 205 */ 206 public function insert($id) 207 { 208 foreach ((is_array($id) ? $id : array($id)) as $val) { 209 if (($val instanceof IMP_Search_Vfolder) && 210 !isset($this->_accounts[strval($val)])) { 211 /* Virtual Folders. */ 212 $account = $this->_accounts[strval($val)] = new IMP_Ftree_Account_Vfolder($val); 213 } elseif (($val instanceof IMP_Remote_Account) && 214 !isset($this->_accounts[strval($val)])) { 215 /* Remote accounts. */ 216 $account = $this->_accounts[strval($val)] = new IMP_Ftree_Account_Remote($val); 217 } else { 218 $account = $this->getAccount($val); 219 $val = $this->_normalize($val); 220 } 221 222 array_map(array($this, '_insertElt'), $account->getList(array($val))); 223 } 224 } 225 226 /** 227 * Expand an element. 228 * 229 * @param mixed $elts The element (or an array of elements) to 230 * expand. 231 * @param boolean $expandall Expand all subelements? 232 */ 233 public function expand($elts, $expandall = false) 234 { 235 foreach ((is_array($elts) ? $elts : array($elts)) as $val) { 236 if (($elt = $this[$val]) && $elt->children) { 237 if (!$elt->open) { 238 $elt->open = true; 239 } 240 241 /* Expand all children beneath this one. */ 242 if ($expandall) { 243 $this->expand($this->_parent[strval($elt)]); 244 } 245 } 246 } 247 } 248 249 /** 250 * Expand all elements. 251 */ 252 public function expandAll() 253 { 254 $this->expand($this->_parent[self::BASE_ELT], true); 255 } 256 257 /** 258 * Collapse an element. 259 * 260 * @param mixed $elts The element (or an array of elements) to expand. 261 */ 262 public function collapse($elts) 263 { 264 foreach ((is_array($elts) ? $elts : array($elts)) as $val) { 265 if ($elt = $this[$val]) { 266 $elt->open = false; 267 } 268 } 269 } 270 271 /** 272 * Collapse all elements. 273 */ 274 public function collapseAll() 275 { 276 $this->collapse( 277 array_diff_key(array_keys($this->_elts), array(self::BASE_ELT)) 278 ); 279 } 280 281 /** 282 * Delete an element from the tree. 283 * 284 * @param mixed $elts The element (or an array of elements) to delete. 285 */ 286 public function delete($id) 287 { 288 if (is_array($id)) { 289 /* We want to delete from the TOP of the tree down to ensure that 290 * parents have an accurate view of what children are left. */ 291 $this->sortList($id); 292 $id = array_reverse($id); 293 } else { 294 $id = array($id); 295 } 296 297 foreach (array_filter(array_map(array($this, 'offsetGet'), $id)) as $elt) { 298 $account = $this->getAccount($elt); 299 if (!($mask = $account->delete($elt))) { 300 continue; 301 } 302 303 $this->_changed = true; 304 305 if ($mask & IMP_Ftree_Account::DELETE_RECURSIVE) { 306 foreach (array_map('strval', iterator_to_array(new IMP_Ftree_Iterator($elt), false)) as $val) { 307 unset( 308 $this->_elts[$val], 309 $this->_parent[$val] 310 ); 311 $this->eltdiff->delete($val); 312 } 313 unset($this->_parent[strval($elt)]); 314 } 315 316 if (strval($account) == strval($elt)) { 317 unset($this->_accounts[strval($elt)]); 318 } 319 320 if ($mask & IMP_Ftree_Account::DELETE_ELEMENT) { 321 /* Do not delete from tree if there are child elements - 322 * instead, convert to a container element. */ 323 if ($elt->children) { 324 $elt->container = true; 325 continue; 326 } 327 328 /* Remove the mailbox from the expanded folders list. */ 329 unset($this->expanded[$elt]); 330 331 /* Remove the mailbox from the polled list. */ 332 $this->poll->removePollList($elt); 333 } 334 335 $parent = strval($elt->parent); 336 $this->eltdiff->delete($elt); 337 338 /* Delete the entry from the parent tree. */ 339 unset( 340 $this->_elts[strval($elt)], 341 $this->_parent[$parent][array_search(strval($elt), $this->_parent[$parent], true)] 342 ); 343 344 if (empty($this->_parent[$parent])) { 345 /* This mailbox is now completely empty (no children). */ 346 unset($this->_parent[$parent]); 347 if ($p_elt = $this[$parent]) { 348 if ($p_elt->container && !$p_elt->namespace) { 349 $this->delete($p_elt); 350 } else { 351 $p_elt->open = false; 352 $this->eltdiff->change($p_elt); 353 } 354 } 355 } 356 357 if (!empty($this->_parent[$parent])) { 358 $this->_parent[$parent] = array_values($this->_parent[$parent]); 359 } 360 } 361 } 362 363 /** 364 * Rename a mailbox. 365 * 366 * @param string $old The old mailbox name. 367 * @param string $new The new mailbox name. 368 */ 369 public function rename($old, $new) 370 { 371 if (!($old_elt = $this[$old])) { 372 return; 373 } 374 375 $new_list = $polled = array(); 376 $old_list = array_merge( 377 array($old), 378 iterator_to_array(new IMP_Ftree_IteratorFilter(new IMP_Ftree_Iterator($old_elt)), false) 379 ); 380 381 foreach ($old_list as $val) { 382 $new_list[] = $new_name = substr_replace($val, $new, 0, strlen($old)); 383 if ($val->polled) { 384 $polled[] = $new_name; 385 } 386 } 387 388 $this->insert($new_list); 389 $this->poll->addPollList($polled); 390 $this->delete($old_list); 391 } 392 393 /** 394 * Subscribe an element to the tree. 395 * 396 * @param mixed $id The element name or an array of element names. 397 */ 398 public function subscribe($id) 399 { 400 foreach ((is_array($id) ? $id : array($id)) as $val) { 401 $this->setAttribute('subscribed', $val, true); 402 $this->setAttribute('container', $val, false); 403 } 404 } 405 406 /** 407 * Unsubscribe an element from the tree. 408 * 409 * @param mixed $id The element name or an array of element names. 410 */ 411 public function unsubscribe($id) 412 { 413 if (is_array($id)) { 414 /* We want to delete from the TOP of the tree down to ensure that 415 * parents have an accurate view of what children are left. */ 416 $this->sortList($id); 417 $id = array_reverse($id); 418 } else { 419 $id = array($id); 420 } 421 422 foreach ($id as $val) { 423 /* INBOX can never be unsubscribed to. */ 424 if (($elt = $this[$val]) && !$elt->inbox) { 425 $this->_changed = true; 426 427 /* Do not delete from tree if there are child elements - 428 * instead, convert to a container element. */ 429 if ($elt->children) { 430 $this->setAttribute('container', $elt, true); 431 } 432 433 /* Set as unsubscribed, add to unsubscribed list, and remove 434 * from subscribed list. */ 435 $this->setAttribute('subscribed', $elt, false); 436 } 437 } 438 } 439 440 /** 441 * Load unsubscribed mailboxes. 442 */ 443 public function loadUnsubscribed() 444 { 445 /* If we are switching from unsubscribed to subscribed, no need 446 * to do anything (we just ignore unsubscribed stuff). */ 447 if ($this->unsubscribed_loaded) { 448 return; 449 } 450 451 $this->_changed = true; 452 453 /* The BASE_ELT having the SUBSCRIBED mask indicates the unsubscribed 454 * mailboxes have been loaded into the object. */ 455 $this->setAttribute('subscribed', self::BASE_ELT, true); 456 457 /* If we are switching from subscribed to unsubscribed, we need 458 * to add all unsubscribed elements that live in currently 459 * discovered items. */ 460 $old_track = $this->eltdiff->track; 461 $this->eltdiff->track = false; 462 foreach ($this->_accounts as $val) { 463 array_map(array($this, '_insertElt'), $val->getList(array(), $val::UNSUB)); 464 } 465 $this->eltdiff->track = $old_track; 466 } 467 468 /** 469 * Get an attribute value. 470 * 471 * @param string $type The attribute type. 472 * @param string $name The element name. 473 * 474 * @return mixed Boolean attribute result, or null if element or 475 * attribute doesn't exist 476 */ 477 public function getAttribute($type, $name) 478 { 479 if (!($elt = $this[$name])) { 480 return null; 481 } 482 $s_elt = strval($elt); 483 484 switch ($type) { 485 case 'children': 486 return isset($this->_parent[$s_elt]); 487 488 case 'container': 489 $attr = self::ELT_NOSELECT; 490 break; 491 492 case 'invisible': 493 $attr = self::ELT_INVISIBLE; 494 break; 495 496 case 'namespace_other': 497 $attr = self::ELT_NAMESPACE_OTHER; 498 break; 499 500 case 'namespace_shared': 501 $attr = self::ELT_NAMESPACE_SHARED; 502 break; 503 504 case 'needsort': 505 $attr = self::ELT_NEED_SORT; 506 break; 507 508 case 'nochildren': 509 $attr = self::ELT_NOINFERIORS; 510 break; 511 512 case 'nonimap': 513 $attr = self::ELT_NONIMAP; 514 break; 515 516 case 'open': 517 if (!$elt->children) { 518 return false; 519 } 520 $attr = self::ELT_IS_OPEN; 521 break; 522 523 case 'polled': 524 if ($this->_elts[$s_elt] & self::ELT_IS_POLLED) { 525 return true; 526 } elseif ($this->_elts[$s_elt] & self::ELT_NOT_POLLED) { 527 return false; 528 } 529 530 $polled = $this->poll[$elt]; 531 $this->setAttribute('polled', $elt, $polled); 532 return $polled; 533 534 case 'remote': 535 $attr = self::ELT_REMOTE; 536 break; 537 538 case 'remote_auth': 539 $attr = self::ELT_REMOTE_AUTH; 540 break; 541 542 case 'remote_mbox': 543 $attr = self::ELT_REMOTE_MBOX; 544 break; 545 546 case 'subscribed': 547 if ($elt->inbox) { 548 return true; 549 } 550 $attr = self::ELT_IS_SUBSCRIBED; 551 break; 552 553 case 'vfolder': 554 $attr = self::ELT_VFOLDER; 555 break; 556 557 default: 558 return null; 559 } 560 561 return (bool)($this->_elts[$s_elt] & $attr); 562 } 563 564 /** 565 * Change an attribute value. 566 * 567 * @param string $type The attribute type. 568 * @param string $elt The element name. 569 * @param boolean $bool The boolean value. 570 */ 571 public function setAttribute($type, $elt, $bool) 572 { 573 if (!($elt = $this[$elt])) { 574 return; 575 } 576 577 $attr = null; 578 $s_elt = strval($elt); 579 580 switch ($type) { 581 case 'container': 582 $attr = self::ELT_NOSELECT; 583 $this->eltdiff->change($elt); 584 break; 585 586 case 'invisible': 587 $attr = self::ELT_INVISIBLE; 588 $this->eltdiff->change($elt); 589 break; 590 591 case 'needsort': 592 $attr = self::ELT_NEED_SORT; 593 break; 594 595 case 'open': 596 $attr = self::ELT_IS_OPEN; 597 if ($bool) { 598 $this->expanded[$elt] = true; 599 } else { 600 unset($this->expanded[$elt]); 601 } 602 break; 603 604 case 'polled': 605 if ($bool) { 606 $attr = self::ELT_IS_POLLED; 607 $remove = self::ELT_NOT_POLLED; 608 } else { 609 $attr = self::ELT_NOT_POLLED; 610 $remove = self::ELT_IS_POLLED; 611 } 612 $this->_elts[$s_elt] &= ~$remove; 613 break; 614 615 case 'subscribed': 616 $attr = self::ELT_IS_SUBSCRIBED; 617 $this->eltdiff->change($elt); 618 break; 619 620 default: 621 return; 622 } 623 624 if ($bool) { 625 $this->_elts[$s_elt] |= $attr; 626 } else { 627 $this->_elts[$s_elt] &= ~$attr; 628 } 629 630 $this->_changed = true; 631 } 632 633 /** 634 * Get the account object for a given element ID. 635 * 636 * @param string $id Element ID. 637 * 638 * @return IMP_Ftree_Account Account object. 639 */ 640 public function getAccount($id) 641 { 642 foreach (array_diff(array_keys($this->_accounts), array(self::BASE_ELT)) as $val) { 643 if (strpos($id, $val) === 0) { 644 return $this->_accounts[$val]; 645 } 646 } 647 648 return $this->_accounts[self::BASE_ELT]; 649 } 650 651 /** 652 * Return the list of children for a given element ID. 653 * 654 * @param string $id Element ID. 655 * 656 * @return array Array of tree elements. 657 */ 658 public function getChildren($id) 659 { 660 if (!($elt = $this[$id]) || !isset($this->_parent[strval($elt)])) { 661 return array(); 662 } 663 664 $this->_sortLevel($elt); 665 return array_map( 666 array($this, 'offsetGet'), $this->_parent[strval($elt)] 667 ); 668 } 669 670 /** 671 * Get the parent element for a given element ID. 672 * 673 * @param string $id Element ID. 674 * 675 * @return mixed IMP_Ftree_Element object, or null if no parent. 676 */ 677 public function getParent($id) 678 { 679 $id = strval($id); 680 681 if ($id == self::BASE_ELT) { 682 return null; 683 } 684 685 foreach ($this->_parent as $key => $val) { 686 if (in_array($id, $val, true)) { 687 return $this[$key]; 688 } 689 } 690 691 return $this[self::BASE_ELT]; 692 } 693 694 /** 695 * Sorts a list of mailboxes. 696 * 697 * @param array &$mbox The list of mailboxes to sort. 698 * @param IMP_Ftree_Element $base The base element. 699 */ 700 public function sortList(&$mbox, $base = false) 701 { 702 if (count($mbox) < 2) { 703 return; 704 } 705 706 if (!$base || (!$base->base_elt && !$base->remote_auth)) { 707 $list_ob = new Horde_Imap_Client_Mailbox_List($mbox); 708 $mbox = $list_ob->sort(); 709 return; 710 } 711 712 $prefix = $base->base_elt 713 ? '' 714 : (strval($this->getAccount($base)) . "\0"); 715 716 $basesort = $othersort = array(); 717 /* INBOX always appears first. */ 718 $sorted = array($prefix . 'INBOX'); 719 720 foreach ($mbox as $key => $val) { 721 $ob = $this[$val]; 722 if ($ob->nonimap) { 723 $othersort[$key] = $ob->mbox_ob->label; 724 } elseif ($val !== ($prefix . 'INBOX')) { 725 $basesort[$key] = $ob->mbox_ob->label; 726 } 727 } 728 729 natcasesort($basesort); 730 natcasesort($othersort); 731 foreach (array_merge(array_keys($basesort), array_keys($othersort)) as $key) { 732 $sorted[] = $mbox[$key]; 733 } 734 735 $mbox = $sorted; 736 } 737 738 739 /* Internal methods. */ 740 741 /** 742 * Normalize an element ID to the correct, internal name. 743 * 744 * @param string $id The element ID. 745 * 746 * @return string The converted name. 747 */ 748 protected function _normalize($id) 749 { 750 $id = strval($id); 751 752 return (strcasecmp($id, 'INBOX') === 0) 753 ? 'INBOX' 754 : $id; 755 } 756 757 /** 758 * Insert an element into the tree. 759 * 760 * @param array $elt Element data. Keys: 761 * <pre> 762 * - a: (integer) Attributes. 763 * - p: (string) Parent element ID. 764 * - v: (string) Mailbox ID. 765 * </pre> 766 */ 767 protected function _insertElt($elt) 768 { 769 $name = $this->_normalize($elt['v']); 770 771 $change = false; 772 if (isset($this->_elts[$name])) { 773 if ($elt['a'] & self::ELT_NOSELECT) { 774 return; 775 } 776 $change = true; 777 } 778 779 $p_elt = $this[isset($elt['p']) ? $elt['p'] : self::BASE_ELT]; 780 $parent = strval($p_elt); 781 782 $this->_changed = true; 783 784 if (!isset($this->_parent[$parent])) { 785 $this->eltdiff->change($p_elt); 786 } 787 if (!isset($this->_elts[$name])) { 788 $this->_parent[$parent][] = $name; 789 } 790 $this->_elts[$name] = $elt['a']; 791 792 if ($change) { 793 $this->eltdiff->change($name); 794 } else { 795 $this->eltdiff->add($name); 796 } 797 798 /* Check for polled status. */ 799 $this->setAttribute('polled', $name, $this->poll[$name]); 800 801 /* Check for expanded status. */ 802 $this->setAttribute('open', $name, $this->expanded[$name]); 803 804 if (empty($this->_temp['nohook'])) { 805 try { 806 $this->setAttribute( 807 'invisible', 808 $name, 809 !$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->callHook( 810 'display_folder', 811 'imp', 812 array($name) 813 ) 814 ); 815 } catch (Horde_Exception_HookNotSet $e) { 816 $this->_temp['nohook'] = true; 817 } 818 } 819 820 /* Make sure we are sorted correctly. */ 821 $this->setAttribute('needsort', $p_elt, true); 822 } 823 824 /** 825 * Sort a level in the tree. 826 * 827 * @param string $id The parent element whose children need to be sorted. 828 */ 829 protected function _sortLevel($id) 830 { 831 if (($elt = $this[$id]) && $elt->needsort) { 832 if (count($this->_parent[strval($elt)]) > 1) { 833 $this->sortList($this->_parent[strval($elt)], $elt); 834 } 835 $this->setAttribute('needsort', $elt, false); 836 } 837 } 838 839 /* ArrayAccess methods. */ 840 841 /** 842 */ 843 public function offsetExists($offset) 844 { 845 /* Optimization: Only normalize in the rare case it is not found on 846 * the first attempt. */ 847 $offset = strval($offset); 848 return (isset($this->_elts[$offset]) || 849 isset($this->_elts[$this->_normalize($offset)])); 850 } 851 852 /** 853 * @return IMP_Ftree_Element 854 */ 855 public function offsetGet($offset) 856 { 857 if ($offset instanceof IMP_Ftree_Element) { 858 return $offset; 859 } 860 861 /* Optimization: Only normalize in the rare case it is not found on 862 * the first attempt. */ 863 $offset = strval($offset); 864 if (isset($this->_elts[$offset])) { 865 return new IMP_Ftree_Element($offset, $this); 866 } 867 868 $offset = $this->_normalize($offset); 869 return isset($this->_elts[$offset]) 870 ? new IMP_Ftree_Element($offset, $this) 871 : null; 872 } 873 874 /** 875 */ 876 public function offsetSet($offset, $value) 877 { 878 $this->insert($offset); 879 } 880 881 /** 882 */ 883 public function offsetUnset($offset) 884 { 885 $this->delete($offset); 886 } 887 888 /* Countable methods. */ 889 890 /** 891 * Return the number of mailboxes on the server. 892 */ 893 public function count() 894 { 895 $this->loadUnsubscribed(); 896 897 $iterator = new IMP_Ftree_IteratorFilter($this); 898 $iterator->add($iterator::NONIMAP); 899 $iterator->remove($iterator::UNSUB); 900 901 return iterator_count($iterator); 902 } 903 904 /* Serializable methods. */ 905 906 /** 907 */ 908 public function serialize() 909 { 910 return $GLOBALS['injector']->getInstance('Horde_Pack')->pack(array( 911 $this->_accounts, 912 $this->_eltdiff, 913 $this->_elts, 914 $this->_parent 915 ), array( 916 'compress' => false, 917 'phpob' => true 918 )); 919 } 920 921 /** 922 * @throws Horde_Pack_Exception 923 */ 924 public function unserialize($data) 925 { 926 list( 927 $this->_accounts, 928 $this->_eltdiff, 929 $this->_elts, 930 $this->_parent 931 ) = $GLOBALS['injector']->getInstance('Horde_Pack')->unpack($data); 932 } 933 934 /** 935 * Creates a Horde_Tree representation of the current tree. 936 * 937 * @param string|Horde_Tree $name Either the tree name, or a Horde_Tree 938 * object to add nodes to. 939 * @param array $opts Additional options: 940 * <pre> 941 * - basename: (boolean) Use raw basename instead of abbreviated label? 942 * DEFAULT: false 943 * - checkbox: (boolean) Display checkboxes? 944 * DEFAULT: false 945 * - editvfolder: (boolean) Display vfolder edit links? 946 * DEFAULT: false 947 * - iterator: (Iterator) Tree iterator to use. 948 * DEFAULT: Base iterator. 949 * - open: (boolean) Force child mailboxes to this status. 950 * DEFAULT: null 951 * - parent: (string) The parent object of the current level. 952 * DEFAULT: null (add to base level) 953 * - poll_info: (boolean) Include poll information in output? 954 * DEFAULT: false 955 * - render_params: (array) List of params to pass to renderer if 956 * auto-creating. 957 * DEFAULT: 'alternate', 'lines', and 'lines_base' 958 * are passed in with true values. 959 * - render_type: (string) The renderer name. 960 * DEFAULT: Javascript 961 * </pre> 962 * 963 * @return Horde_Tree The tree object. 964 */ 965 public function createTree($name, array $opts = array()) 966 { 967 global $injector, $registry; 968 969 $opts = array_merge(array( 970 'parent' => null, 971 'render_params' => array(), 972 'render_type' => 'Javascript' 973 ), $opts); 974 975 $view = $registry->getView(); 976 977 if ($name instanceof Horde_Tree_Renderer_Base) { 978 $tree = $name; 979 $parent = $opts['parent']; 980 } else { 981 $tree = $injector->getInstance('Horde_Core_Factory_Tree')->create($name, $opts['render_type'], array_merge(array( 982 'alternate' => true, 983 'lines' => true, 984 'lines_base' => true, 985 'nosession' => true 986 ), $opts['render_params'])); 987 $parent = null; 988 } 989 990 $iterator = empty($opts['iterator']) 991 ? new IMP_Ftree_IteratorFilter($this) 992 : $opts['iterator']; 993 994 foreach ($iterator as $val) { 995 $after = ''; 996 $elt_parent = null; 997 $mbox_ob = $val->mbox_ob; 998 $params = array(); 999 1000 switch ($opts['render_type']) { 1001 case 'IMP_Tree_Flist': 1002 if ($mbox_ob->vfolder_container) { 1003 continue 2; 1004 } 1005 1006 $is_open = true; 1007 $label = $params['orig_label'] = empty($opts['basename']) 1008 ? $mbox_ob->abbrev_label 1009 : $mbox_ob->basename; 1010 break; 1011 1012 case 'IMP_Tree_Jquerymobile': 1013 $is_open = true; 1014 $label = $mbox_ob->display_html; 1015 $icon = $mbox_ob->icon; 1016 $params['icon'] = $icon->icon; 1017 $params['special'] = $mbox_ob->inbox || $mbox_ob->special; 1018 $params['class'] = 'imp-folder'; 1019 $params['urlattributes'] = array( 1020 'id' => 'imp-mailbox-' . $mbox_ob->form_to 1021 ); 1022 1023 /* Force to flat tree so that non-polled parents don't cause 1024 * polled children to be skipped by renderer (see Bug 1025 * #11238). */ 1026 $elt_parent = $this[self::BASE_ELT]; 1027 break; 1028 1029 case 'IMP_Tree_Simplehtml': 1030 $is_open = $val->open; 1031 if ($tree->shouldToggle($mbox_ob->form_to)) { 1032 if ($is_open) { 1033 $this->collapse($val); 1034 } else { 1035 $this->expand($val); 1036 } 1037 $is_open = !$is_open; 1038 } 1039 $label = htmlspecialchars(Horde_String::abbreviate($mbox_ob->abbrev_label, 30 - ($val->level * 2))); 1040 break; 1041 1042 case 'Javascript': 1043 $is_open = $val->open; 1044 $label = empty($opts['basename']) 1045 ? htmlspecialchars($mbox_ob->abbrev_label) 1046 : htmlspecialchars($mbox_ob->basename); 1047 $icon = $mbox_ob->icon; 1048 $params['icon'] = $icon->icon; 1049 $params['iconopen'] = $icon->iconopen; 1050 break; 1051 } 1052 1053 if (!empty($opts['poll_info']) && $val->polled) { 1054 $poll_info = $mbox_ob->poll_info; 1055 1056 if ($poll_info->unseen) { 1057 switch ($opts['render_type']) { 1058 case 'IMP_Tree_Jquerymobile': 1059 $after = $poll_info->unseen; 1060 break; 1061 1062 default: 1063 $label = '<strong>' . $label . '</strong> (' . 1064 $poll_info->unseen . ')'; 1065 } 1066 } 1067 } 1068 1069 if ($val->container) { 1070 $params['container'] = true; 1071 } else { 1072 switch ($view) { 1073 case $registry::VIEW_MINIMAL: 1074 $params['url'] = IMP_Minimal_Mailbox::url(array('mailbox' => $mbox_ob)); 1075 break; 1076 1077 case $registry::VIEW_SMARTMOBILE: 1078 $url = new Horde_Core_Smartmobile_Url(); 1079 $url->add('mbox', $mbox_ob->form_to); 1080 $url->setAnchor('mailbox'); 1081 $params['url'] = strval($url); 1082 break; 1083 1084 default: 1085 $params['url'] = $mbox_ob->url('mailbox')->setRaw(true); 1086 break; 1087 } 1088 1089 if (!$val->subscribed) { 1090 $params['class'] = 'mboxunsub'; 1091 } 1092 } 1093 1094 $checkbox = empty($opts['checkbox']) 1095 ? '' 1096 : '<input type="checkbox" class="checkbox" name="mbox_list[]" value="' . $mbox_ob->form_to . '"'; 1097 1098 if ($val->nonimap) { 1099 $checkbox .= ' disabled="disabled"'; 1100 } 1101 1102 if ($val->vfolder && 1103 !empty($opts['editvfolder']) && 1104 $val->container) { 1105 $after = ' [' . 1106 $registry->getServiceLink('prefs', 'imp')->add('group', 'searches')->link(array('title' => _("Edit Virtual Folder"))) . _("Edit") . '</a>'. 1107 ']'; 1108 } 1109 1110 if (is_null($elt_parent)) { 1111 $elt_parent = $val->parent; 1112 } 1113 1114 $tree->addNode(array( 1115 'id' => $mbox_ob->form_to, 1116 'parent' => $elt_parent->base_elt ? $parent : $elt_parent->mbox_ob->form_to, 1117 'label' => $label, 1118 'expanded' => isset($opts['open']) ? $opts['open'] : $is_open, 1119 'params' => $params, 1120 'right' => $after, 1121 'left' => empty($opts['checkbox']) ? null : $checkbox . ' />' 1122 )); 1123 } 1124 1125 return $tree; 1126 } 1127 1128 /* IteratorAggregate methods. */ 1129 1130 /** 1131 * This returns a RecursiveIterator - a RecursiveIteratorIterator is 1132 * needed to properly iterate through all elements. 1133 * 1134 * @return IMP_Ftree_Iterator Iterator object. 1135 */ 1136 public function getIterator() 1137 { 1138 return new IMP_Ftree_Iterator($this[self::BASE_ELT]); 1139 } 1140 1141} 1142