1142 lines
33 KiB
PHP
1142 lines
33 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2000-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (GPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/gpl.
|
|
*
|
|
* @category Horde
|
|
* @copyright 2000-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package IMP
|
|
*/
|
|
|
|
/**
|
|
* IMP_Ftree (folder tree) provides a tree view of the mailboxes on a backend
|
|
* (a/k/a a folder list; in IMP, folders = collection of mailboxes), along
|
|
* with other display elements (Remote Accounts; Virtual Folders).
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @author Anil Madhavapeddy <avsm@horde.org>
|
|
* @author Jon Parise <jon@horde.org>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @copyright 2000-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/gpl GPL
|
|
* @package IMP
|
|
*
|
|
* @property-read boolean $changed Has the tree changed?
|
|
* @property-read IMP_Ftree_Eltdiff $eltdiff Element diff tracker.
|
|
* @property-read IMP_FTree_Prefs_Expanded $expanded The expanded folders
|
|
* list.
|
|
* @property-read IMP_Ftree_Prefs_Poll $poll The poll list.
|
|
* @property-read boolean $subscriptions Whether IMAP subscriptions are
|
|
* enabled.
|
|
* @property-read boolean $unsubscribed_loaded True if unsubscribed mailboxes
|
|
* have been loaded.
|
|
*/
|
|
class IMP_Ftree
|
|
implements ArrayAccess, Countable, IteratorAggregate, Serializable
|
|
{
|
|
/* Constants for mailboxElt attributes. */
|
|
const ELT_NOSELECT = 1;
|
|
const ELT_NAMESPACE_OTHER = 2;
|
|
const ELT_NAMESPACE_SHARED = 4;
|
|
const ELT_IS_OPEN = 8;
|
|
const ELT_IS_SUBSCRIBED = 16;
|
|
const ELT_NOINFERIORS = 32;
|
|
const ELT_IS_POLLED = 64;
|
|
const ELT_NOT_POLLED = 128;
|
|
const ELT_VFOLDER = 256;
|
|
const ELT_NONIMAP = 512;
|
|
const ELT_INVISIBLE = 1024;
|
|
const ELT_NEED_SORT = 2048;
|
|
const ELT_REMOTE = 4096;
|
|
const ELT_REMOTE_AUTH = 8192;
|
|
const ELT_REMOTE_MBOX = 16384;
|
|
|
|
/* The string used to indicate the base of the tree. This must include
|
|
* null since this is the only 7-bit character not allowed in IMAP
|
|
* mailboxes (nulls allow us to sort by name but never conflict with an
|
|
* IMAP mailbox). */
|
|
const BASE_ELT = "base\0";
|
|
|
|
/**
|
|
* Account sources.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_accounts;
|
|
|
|
/**
|
|
* Tree changed flag. Set when something in the tree has been altered.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_changed = false;
|
|
|
|
/**
|
|
* Element diff tracking.
|
|
*
|
|
* @var IMP_Ftree_Eltdiff
|
|
*/
|
|
protected $_eltdiff;
|
|
|
|
/**
|
|
* Array containing the mailbox elements.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_elts;
|
|
|
|
/**
|
|
* Parent/child list.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_parent;
|
|
|
|
/**
|
|
* Temporary data that is not saved across serialization.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_temp = array();
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->init();
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function __get($name)
|
|
{
|
|
global $prefs;
|
|
|
|
switch ($name) {
|
|
case 'changed':
|
|
return ($this->_changed || $this->eltdiff->changed);
|
|
|
|
case 'expanded':
|
|
if (!isset($this->_temp['expanded'])) {
|
|
$this->_temp['expanded'] = new IMP_Ftree_Prefs_Expanded();
|
|
}
|
|
return $this->_temp['expanded'];
|
|
|
|
case 'eltdiff':
|
|
return $this->_eltdiff;
|
|
|
|
case 'poll':
|
|
if (!isset($this->_temp['poll'])) {
|
|
$this->_temp['poll'] = new IMP_Ftree_Prefs_Poll($this);
|
|
}
|
|
return $this->_temp['poll'];
|
|
|
|
case 'subscriptions':
|
|
return $prefs->getValue('subscribe');
|
|
|
|
case 'unsubscribed_loaded':
|
|
return $this[self::BASE_ELT]->subscribed;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the tree.
|
|
*/
|
|
public function init()
|
|
{
|
|
global $injector, $session;
|
|
|
|
$access_folders = $injector->getInstance('IMP_Factory_Imap')->create()->access(IMP_Imap::ACCESS_FOLDERS);
|
|
|
|
/* Reset class variables to the defaults. */
|
|
$this->_accounts = $this->_elts = $this->_parent = array();
|
|
$this->_changed = true;
|
|
|
|
$old_track = (isset($this->_eltdiff) && $this->_eltdiff->track);
|
|
$this->_eltdiff = new IMP_Ftree_Eltdiff();
|
|
|
|
/* Create a placeholder element to the base of the tree so we can
|
|
* keep track of whether the base level needs to be sorted. */
|
|
$this->_elts[self::BASE_ELT] = self::ELT_NEED_SORT | self::ELT_NONIMAP;
|
|
$this->_parent[self::BASE_ELT] = array();
|
|
|
|
$mask = IMP_Ftree_Account::INIT;
|
|
if (!$access_folders || !$this->subscriptions || $session->get('imp', 'showunsub')) {
|
|
$mask |= IMP_Ftree_Account::UNSUB;
|
|
$this->setAttribute('subscribed', self::BASE_ELT, true);
|
|
}
|
|
|
|
/* Add base account. */
|
|
$ob = $this->_accounts[self::BASE_ELT] = $access_folders
|
|
? new IMP_Ftree_Account_Imap()
|
|
: new IMP_Ftree_Account_Inboxonly();
|
|
array_map(array($this, '_insertElt'), $ob->getList(null, $mask));
|
|
|
|
if ($access_folders) {
|
|
/* Add remote servers. */
|
|
$this->insert(iterator_to_array(
|
|
$injector->getInstance('IMP_Remote')
|
|
));
|
|
|
|
/* Add virtual folders to the tree. */
|
|
$this->insert(iterator_to_array(
|
|
IMP_Search_IteratorFilter::create(
|
|
IMP_Search_IteratorFilter::VFOLDER
|
|
)
|
|
));
|
|
}
|
|
|
|
if ($old_track) {
|
|
$this->eltdiff->track = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert an element into the tree.
|
|
*
|
|
* @param mixed $id The name of the mailbox (or a list of mailboxes),
|
|
* an IMP_Search_Vfolder object, an IMP_Remote_Account
|
|
* object, or an array containing any mixture of these.
|
|
*/
|
|
public function insert($id)
|
|
{
|
|
foreach ((is_array($id) ? $id : array($id)) as $val) {
|
|
if (($val instanceof IMP_Search_Vfolder) &&
|
|
!isset($this->_accounts[strval($val)])) {
|
|
/* Virtual Folders. */
|
|
$account = $this->_accounts[strval($val)] = new IMP_Ftree_Account_Vfolder($val);
|
|
} elseif (($val instanceof IMP_Remote_Account) &&
|
|
!isset($this->_accounts[strval($val)])) {
|
|
/* Remote accounts. */
|
|
$account = $this->_accounts[strval($val)] = new IMP_Ftree_Account_Remote($val);
|
|
} else {
|
|
$account = $this->getAccount($val);
|
|
$val = $this->_normalize($val);
|
|
}
|
|
|
|
array_map(array($this, '_insertElt'), $account->getList(array($val)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expand an element.
|
|
*
|
|
* @param mixed $elts The element (or an array of elements) to
|
|
* expand.
|
|
* @param boolean $expandall Expand all subelements?
|
|
*/
|
|
public function expand($elts, $expandall = false)
|
|
{
|
|
foreach ((is_array($elts) ? $elts : array($elts)) as $val) {
|
|
if (($elt = $this[$val]) && $elt->children) {
|
|
if (!$elt->open) {
|
|
$elt->open = true;
|
|
}
|
|
|
|
/* Expand all children beneath this one. */
|
|
if ($expandall) {
|
|
$this->expand($this->_parent[strval($elt)]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expand all elements.
|
|
*/
|
|
public function expandAll()
|
|
{
|
|
$this->expand($this->_parent[self::BASE_ELT], true);
|
|
}
|
|
|
|
/**
|
|
* Collapse an element.
|
|
*
|
|
* @param mixed $elts The element (or an array of elements) to expand.
|
|
*/
|
|
public function collapse($elts)
|
|
{
|
|
foreach ((is_array($elts) ? $elts : array($elts)) as $val) {
|
|
if ($elt = $this[$val]) {
|
|
$elt->open = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collapse all elements.
|
|
*/
|
|
public function collapseAll()
|
|
{
|
|
$this->collapse(
|
|
array_diff_key(array_keys($this->_elts), array(self::BASE_ELT))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Delete an element from the tree.
|
|
*
|
|
* @param mixed $elts The element (or an array of elements) to delete.
|
|
*/
|
|
public function delete($id)
|
|
{
|
|
if (is_array($id)) {
|
|
/* We want to delete from the TOP of the tree down to ensure that
|
|
* parents have an accurate view of what children are left. */
|
|
$this->sortList($id);
|
|
$id = array_reverse($id);
|
|
} else {
|
|
$id = array($id);
|
|
}
|
|
|
|
foreach (array_filter(array_map(array($this, 'offsetGet'), $id)) as $elt) {
|
|
$account = $this->getAccount($elt);
|
|
if (!($mask = $account->delete($elt))) {
|
|
continue;
|
|
}
|
|
|
|
$this->_changed = true;
|
|
|
|
if ($mask & IMP_Ftree_Account::DELETE_RECURSIVE) {
|
|
foreach (array_map('strval', iterator_to_array(new IMP_Ftree_Iterator($elt), false)) as $val) {
|
|
unset(
|
|
$this->_elts[$val],
|
|
$this->_parent[$val]
|
|
);
|
|
$this->eltdiff->delete($val);
|
|
}
|
|
unset($this->_parent[strval($elt)]);
|
|
}
|
|
|
|
if (strval($account) == strval($elt)) {
|
|
unset($this->_accounts[strval($elt)]);
|
|
}
|
|
|
|
if ($mask & IMP_Ftree_Account::DELETE_ELEMENT) {
|
|
/* Do not delete from tree if there are child elements -
|
|
* instead, convert to a container element. */
|
|
if ($elt->children) {
|
|
$elt->container = true;
|
|
continue;
|
|
}
|
|
|
|
/* Remove the mailbox from the expanded folders list. */
|
|
unset($this->expanded[$elt]);
|
|
|
|
/* Remove the mailbox from the polled list. */
|
|
$this->poll->removePollList($elt);
|
|
}
|
|
|
|
$parent = strval($elt->parent);
|
|
$this->eltdiff->delete($elt);
|
|
|
|
/* Delete the entry from the parent tree. */
|
|
unset(
|
|
$this->_elts[strval($elt)],
|
|
$this->_parent[$parent][array_search(strval($elt), $this->_parent[$parent], true)]
|
|
);
|
|
|
|
if (empty($this->_parent[$parent])) {
|
|
/* This mailbox is now completely empty (no children). */
|
|
unset($this->_parent[$parent]);
|
|
if ($p_elt = $this[$parent]) {
|
|
if ($p_elt->container && !$p_elt->namespace) {
|
|
$this->delete($p_elt);
|
|
} else {
|
|
$p_elt->open = false;
|
|
$this->eltdiff->change($p_elt);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($this->_parent[$parent])) {
|
|
$this->_parent[$parent] = array_values($this->_parent[$parent]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rename a mailbox.
|
|
*
|
|
* @param string $old The old mailbox name.
|
|
* @param string $new The new mailbox name.
|
|
*/
|
|
public function rename($old, $new)
|
|
{
|
|
if (!($old_elt = $this[$old])) {
|
|
return;
|
|
}
|
|
|
|
$new_list = $polled = array();
|
|
$old_list = array_merge(
|
|
array($old),
|
|
iterator_to_array(new IMP_Ftree_IteratorFilter(new IMP_Ftree_Iterator($old_elt)), false)
|
|
);
|
|
|
|
foreach ($old_list as $val) {
|
|
$new_list[] = $new_name = substr_replace($val, $new, 0, strlen($old));
|
|
if ($val->polled) {
|
|
$polled[] = $new_name;
|
|
}
|
|
}
|
|
|
|
$this->insert($new_list);
|
|
$this->poll->addPollList($polled);
|
|
$this->delete($old_list);
|
|
}
|
|
|
|
/**
|
|
* Subscribe an element to the tree.
|
|
*
|
|
* @param mixed $id The element name or an array of element names.
|
|
*/
|
|
public function subscribe($id)
|
|
{
|
|
foreach ((is_array($id) ? $id : array($id)) as $val) {
|
|
$this->setAttribute('subscribed', $val, true);
|
|
$this->setAttribute('container', $val, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe an element from the tree.
|
|
*
|
|
* @param mixed $id The element name or an array of element names.
|
|
*/
|
|
public function unsubscribe($id)
|
|
{
|
|
if (is_array($id)) {
|
|
/* We want to delete from the TOP of the tree down to ensure that
|
|
* parents have an accurate view of what children are left. */
|
|
$this->sortList($id);
|
|
$id = array_reverse($id);
|
|
} else {
|
|
$id = array($id);
|
|
}
|
|
|
|
foreach ($id as $val) {
|
|
/* INBOX can never be unsubscribed to. */
|
|
if (($elt = $this[$val]) && !$elt->inbox) {
|
|
$this->_changed = true;
|
|
|
|
/* Do not delete from tree if there are child elements -
|
|
* instead, convert to a container element. */
|
|
if ($elt->children) {
|
|
$this->setAttribute('container', $elt, true);
|
|
}
|
|
|
|
/* Set as unsubscribed, add to unsubscribed list, and remove
|
|
* from subscribed list. */
|
|
$this->setAttribute('subscribed', $elt, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load unsubscribed mailboxes.
|
|
*/
|
|
public function loadUnsubscribed()
|
|
{
|
|
/* If we are switching from unsubscribed to subscribed, no need
|
|
* to do anything (we just ignore unsubscribed stuff). */
|
|
if ($this->unsubscribed_loaded) {
|
|
return;
|
|
}
|
|
|
|
$this->_changed = true;
|
|
|
|
/* The BASE_ELT having the SUBSCRIBED mask indicates the unsubscribed
|
|
* mailboxes have been loaded into the object. */
|
|
$this->setAttribute('subscribed', self::BASE_ELT, true);
|
|
|
|
/* If we are switching from subscribed to unsubscribed, we need
|
|
* to add all unsubscribed elements that live in currently
|
|
* discovered items. */
|
|
$old_track = $this->eltdiff->track;
|
|
$this->eltdiff->track = false;
|
|
foreach ($this->_accounts as $val) {
|
|
array_map(array($this, '_insertElt'), $val->getList(array(), $val::UNSUB));
|
|
}
|
|
$this->eltdiff->track = $old_track;
|
|
}
|
|
|
|
/**
|
|
* Get an attribute value.
|
|
*
|
|
* @param string $type The attribute type.
|
|
* @param string $name The element name.
|
|
*
|
|
* @return mixed Boolean attribute result, or null if element or
|
|
* attribute doesn't exist
|
|
*/
|
|
public function getAttribute($type, $name)
|
|
{
|
|
if (!($elt = $this[$name])) {
|
|
return null;
|
|
}
|
|
$s_elt = strval($elt);
|
|
|
|
switch ($type) {
|
|
case 'children':
|
|
return isset($this->_parent[$s_elt]);
|
|
|
|
case 'container':
|
|
$attr = self::ELT_NOSELECT;
|
|
break;
|
|
|
|
case 'invisible':
|
|
$attr = self::ELT_INVISIBLE;
|
|
break;
|
|
|
|
case 'namespace_other':
|
|
$attr = self::ELT_NAMESPACE_OTHER;
|
|
break;
|
|
|
|
case 'namespace_shared':
|
|
$attr = self::ELT_NAMESPACE_SHARED;
|
|
break;
|
|
|
|
case 'needsort':
|
|
$attr = self::ELT_NEED_SORT;
|
|
break;
|
|
|
|
case 'nochildren':
|
|
$attr = self::ELT_NOINFERIORS;
|
|
break;
|
|
|
|
case 'nonimap':
|
|
$attr = self::ELT_NONIMAP;
|
|
break;
|
|
|
|
case 'open':
|
|
if (!$elt->children) {
|
|
return false;
|
|
}
|
|
$attr = self::ELT_IS_OPEN;
|
|
break;
|
|
|
|
case 'polled':
|
|
if ($this->_elts[$s_elt] & self::ELT_IS_POLLED) {
|
|
return true;
|
|
} elseif ($this->_elts[$s_elt] & self::ELT_NOT_POLLED) {
|
|
return false;
|
|
}
|
|
|
|
$polled = $this->poll[$elt];
|
|
$this->setAttribute('polled', $elt, $polled);
|
|
return $polled;
|
|
|
|
case 'remote':
|
|
$attr = self::ELT_REMOTE;
|
|
break;
|
|
|
|
case 'remote_auth':
|
|
$attr = self::ELT_REMOTE_AUTH;
|
|
break;
|
|
|
|
case 'remote_mbox':
|
|
$attr = self::ELT_REMOTE_MBOX;
|
|
break;
|
|
|
|
case 'subscribed':
|
|
if ($elt->inbox) {
|
|
return true;
|
|
}
|
|
$attr = self::ELT_IS_SUBSCRIBED;
|
|
break;
|
|
|
|
case 'vfolder':
|
|
$attr = self::ELT_VFOLDER;
|
|
break;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
return (bool)($this->_elts[$s_elt] & $attr);
|
|
}
|
|
|
|
/**
|
|
* Change an attribute value.
|
|
*
|
|
* @param string $type The attribute type.
|
|
* @param string $elt The element name.
|
|
* @param boolean $bool The boolean value.
|
|
*/
|
|
public function setAttribute($type, $elt, $bool)
|
|
{
|
|
if (!($elt = $this[$elt])) {
|
|
return;
|
|
}
|
|
|
|
$attr = null;
|
|
$s_elt = strval($elt);
|
|
|
|
switch ($type) {
|
|
case 'container':
|
|
$attr = self::ELT_NOSELECT;
|
|
$this->eltdiff->change($elt);
|
|
break;
|
|
|
|
case 'invisible':
|
|
$attr = self::ELT_INVISIBLE;
|
|
$this->eltdiff->change($elt);
|
|
break;
|
|
|
|
case 'needsort':
|
|
$attr = self::ELT_NEED_SORT;
|
|
break;
|
|
|
|
case 'open':
|
|
$attr = self::ELT_IS_OPEN;
|
|
if ($bool) {
|
|
$this->expanded[$elt] = true;
|
|
} else {
|
|
unset($this->expanded[$elt]);
|
|
}
|
|
break;
|
|
|
|
case 'polled':
|
|
if ($bool) {
|
|
$attr = self::ELT_IS_POLLED;
|
|
$remove = self::ELT_NOT_POLLED;
|
|
} else {
|
|
$attr = self::ELT_NOT_POLLED;
|
|
$remove = self::ELT_IS_POLLED;
|
|
}
|
|
$this->_elts[$s_elt] &= ~$remove;
|
|
break;
|
|
|
|
case 'subscribed':
|
|
$attr = self::ELT_IS_SUBSCRIBED;
|
|
$this->eltdiff->change($elt);
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if ($bool) {
|
|
$this->_elts[$s_elt] |= $attr;
|
|
} else {
|
|
$this->_elts[$s_elt] &= ~$attr;
|
|
}
|
|
|
|
$this->_changed = true;
|
|
}
|
|
|
|
/**
|
|
* Get the account object for a given element ID.
|
|
*
|
|
* @param string $id Element ID.
|
|
*
|
|
* @return IMP_Ftree_Account Account object.
|
|
*/
|
|
public function getAccount($id)
|
|
{
|
|
foreach (array_diff(array_keys($this->_accounts), array(self::BASE_ELT)) as $val) {
|
|
if (strpos($id, $val) === 0) {
|
|
return $this->_accounts[$val];
|
|
}
|
|
}
|
|
|
|
return $this->_accounts[self::BASE_ELT];
|
|
}
|
|
|
|
/**
|
|
* Return the list of children for a given element ID.
|
|
*
|
|
* @param string $id Element ID.
|
|
*
|
|
* @return array Array of tree elements.
|
|
*/
|
|
public function getChildren($id)
|
|
{
|
|
if (!($elt = $this[$id]) || !isset($this->_parent[strval($elt)])) {
|
|
return array();
|
|
}
|
|
|
|
$this->_sortLevel($elt);
|
|
return array_map(
|
|
array($this, 'offsetGet'), $this->_parent[strval($elt)]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the parent element for a given element ID.
|
|
*
|
|
* @param string $id Element ID.
|
|
*
|
|
* @return mixed IMP_Ftree_Element object, or null if no parent.
|
|
*/
|
|
public function getParent($id)
|
|
{
|
|
$id = strval($id);
|
|
|
|
if ($id == self::BASE_ELT) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($this->_parent as $key => $val) {
|
|
if (in_array($id, $val, true)) {
|
|
return $this[$key];
|
|
}
|
|
}
|
|
|
|
return $this[self::BASE_ELT];
|
|
}
|
|
|
|
/**
|
|
* Sorts a list of mailboxes.
|
|
*
|
|
* @param array &$mbox The list of mailboxes to sort.
|
|
* @param IMP_Ftree_Element $base The base element.
|
|
*/
|
|
public function sortList(&$mbox, $base = false)
|
|
{
|
|
if (count($mbox) < 2) {
|
|
return;
|
|
}
|
|
|
|
if (!$base || (!$base->base_elt && !$base->remote_auth)) {
|
|
$list_ob = new Horde_Imap_Client_Mailbox_List($mbox);
|
|
$mbox = $list_ob->sort();
|
|
return;
|
|
}
|
|
|
|
$prefix = $base->base_elt
|
|
? ''
|
|
: (strval($this->getAccount($base)) . "\0");
|
|
|
|
$basesort = $othersort = array();
|
|
/* INBOX always appears first. */
|
|
$sorted = array($prefix . 'INBOX');
|
|
|
|
foreach ($mbox as $key => $val) {
|
|
$ob = $this[$val];
|
|
if ($ob->nonimap) {
|
|
$othersort[$key] = $ob->mbox_ob->label;
|
|
} elseif ($val !== ($prefix . 'INBOX')) {
|
|
$basesort[$key] = $ob->mbox_ob->label;
|
|
}
|
|
}
|
|
|
|
natcasesort($basesort);
|
|
natcasesort($othersort);
|
|
foreach (array_merge(array_keys($basesort), array_keys($othersort)) as $key) {
|
|
$sorted[] = $mbox[$key];
|
|
}
|
|
|
|
$mbox = $sorted;
|
|
}
|
|
|
|
|
|
/* Internal methods. */
|
|
|
|
/**
|
|
* Normalize an element ID to the correct, internal name.
|
|
*
|
|
* @param string $id The element ID.
|
|
*
|
|
* @return string The converted name.
|
|
*/
|
|
protected function _normalize($id)
|
|
{
|
|
$id = strval($id);
|
|
|
|
return (strcasecmp($id, 'INBOX') === 0)
|
|
? 'INBOX'
|
|
: $id;
|
|
}
|
|
|
|
/**
|
|
* Insert an element into the tree.
|
|
*
|
|
* @param array $elt Element data. Keys:
|
|
* <pre>
|
|
* - a: (integer) Attributes.
|
|
* - p: (string) Parent element ID.
|
|
* - v: (string) Mailbox ID.
|
|
* </pre>
|
|
*/
|
|
protected function _insertElt($elt)
|
|
{
|
|
$name = $this->_normalize($elt['v']);
|
|
|
|
$change = false;
|
|
if (isset($this->_elts[$name])) {
|
|
if ($elt['a'] & self::ELT_NOSELECT) {
|
|
return;
|
|
}
|
|
$change = true;
|
|
}
|
|
|
|
$p_elt = $this[isset($elt['p']) ? $elt['p'] : self::BASE_ELT];
|
|
$parent = strval($p_elt);
|
|
|
|
$this->_changed = true;
|
|
|
|
if (!isset($this->_parent[$parent])) {
|
|
$this->eltdiff->change($p_elt);
|
|
}
|
|
if (!isset($this->_elts[$name])) {
|
|
$this->_parent[$parent][] = $name;
|
|
}
|
|
$this->_elts[$name] = $elt['a'];
|
|
|
|
if ($change) {
|
|
$this->eltdiff->change($name);
|
|
} else {
|
|
$this->eltdiff->add($name);
|
|
}
|
|
|
|
/* Check for polled status. */
|
|
$this->setAttribute('polled', $name, $this->poll[$name]);
|
|
|
|
/* Check for expanded status. */
|
|
$this->setAttribute('open', $name, $this->expanded[$name]);
|
|
|
|
if (empty($this->_temp['nohook'])) {
|
|
try {
|
|
$this->setAttribute(
|
|
'invisible',
|
|
$name,
|
|
!$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->callHook(
|
|
'display_folder',
|
|
'imp',
|
|
array($name)
|
|
)
|
|
);
|
|
} catch (Horde_Exception_HookNotSet $e) {
|
|
$this->_temp['nohook'] = true;
|
|
}
|
|
}
|
|
|
|
/* Make sure we are sorted correctly. */
|
|
$this->setAttribute('needsort', $p_elt, true);
|
|
}
|
|
|
|
/**
|
|
* Sort a level in the tree.
|
|
*
|
|
* @param string $id The parent element whose children need to be sorted.
|
|
*/
|
|
protected function _sortLevel($id)
|
|
{
|
|
if (($elt = $this[$id]) && $elt->needsort) {
|
|
if (count($this->_parent[strval($elt)]) > 1) {
|
|
$this->sortList($this->_parent[strval($elt)], $elt);
|
|
}
|
|
$this->setAttribute('needsort', $elt, false);
|
|
}
|
|
}
|
|
|
|
/* ArrayAccess methods. */
|
|
|
|
/**
|
|
*/
|
|
public function offsetExists($offset)
|
|
{
|
|
/* Optimization: Only normalize in the rare case it is not found on
|
|
* the first attempt. */
|
|
$offset = strval($offset);
|
|
return (isset($this->_elts[$offset]) ||
|
|
isset($this->_elts[$this->_normalize($offset)]));
|
|
}
|
|
|
|
/**
|
|
* @return IMP_Ftree_Element
|
|
*/
|
|
public function offsetGet($offset)
|
|
{
|
|
if ($offset instanceof IMP_Ftree_Element) {
|
|
return $offset;
|
|
}
|
|
|
|
/* Optimization: Only normalize in the rare case it is not found on
|
|
* the first attempt. */
|
|
$offset = strval($offset);
|
|
if (isset($this->_elts[$offset])) {
|
|
return new IMP_Ftree_Element($offset, $this);
|
|
}
|
|
|
|
$offset = $this->_normalize($offset);
|
|
return isset($this->_elts[$offset])
|
|
? new IMP_Ftree_Element($offset, $this)
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function offsetSet($offset, $value)
|
|
{
|
|
$this->insert($offset);
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function offsetUnset($offset)
|
|
{
|
|
$this->delete($offset);
|
|
}
|
|
|
|
/* Countable methods. */
|
|
|
|
/**
|
|
* Return the number of mailboxes on the server.
|
|
*/
|
|
public function count()
|
|
{
|
|
$this->loadUnsubscribed();
|
|
|
|
$iterator = new IMP_Ftree_IteratorFilter($this);
|
|
$iterator->add($iterator::NONIMAP);
|
|
$iterator->remove($iterator::UNSUB);
|
|
|
|
return iterator_count($iterator);
|
|
}
|
|
|
|
/* Serializable methods. */
|
|
|
|
/**
|
|
*/
|
|
public function serialize()
|
|
{
|
|
return $GLOBALS['injector']->getInstance('Horde_Pack')->pack(array(
|
|
$this->_accounts,
|
|
$this->_eltdiff,
|
|
$this->_elts,
|
|
$this->_parent
|
|
), array(
|
|
'compress' => false,
|
|
'phpob' => true
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @throws Horde_Pack_Exception
|
|
*/
|
|
public function unserialize($data)
|
|
{
|
|
list(
|
|
$this->_accounts,
|
|
$this->_eltdiff,
|
|
$this->_elts,
|
|
$this->_parent
|
|
) = $GLOBALS['injector']->getInstance('Horde_Pack')->unpack($data);
|
|
}
|
|
|
|
/**
|
|
* Creates a Horde_Tree representation of the current tree.
|
|
*
|
|
* @param string|Horde_Tree $name Either the tree name, or a Horde_Tree
|
|
* object to add nodes to.
|
|
* @param array $opts Additional options:
|
|
* <pre>
|
|
* - basename: (boolean) Use raw basename instead of abbreviated label?
|
|
* DEFAULT: false
|
|
* - checkbox: (boolean) Display checkboxes?
|
|
* DEFAULT: false
|
|
* - editvfolder: (boolean) Display vfolder edit links?
|
|
* DEFAULT: false
|
|
* - iterator: (Iterator) Tree iterator to use.
|
|
* DEFAULT: Base iterator.
|
|
* - open: (boolean) Force child mailboxes to this status.
|
|
* DEFAULT: null
|
|
* - parent: (string) The parent object of the current level.
|
|
* DEFAULT: null (add to base level)
|
|
* - poll_info: (boolean) Include poll information in output?
|
|
* DEFAULT: false
|
|
* - render_params: (array) List of params to pass to renderer if
|
|
* auto-creating.
|
|
* DEFAULT: 'alternate', 'lines', and 'lines_base'
|
|
* are passed in with true values.
|
|
* - render_type: (string) The renderer name.
|
|
* DEFAULT: Javascript
|
|
* </pre>
|
|
*
|
|
* @return Horde_Tree The tree object.
|
|
*/
|
|
public function createTree($name, array $opts = array())
|
|
{
|
|
global $injector, $registry;
|
|
|
|
$opts = array_merge(array(
|
|
'parent' => null,
|
|
'render_params' => array(),
|
|
'render_type' => 'Javascript'
|
|
), $opts);
|
|
|
|
$view = $registry->getView();
|
|
|
|
if ($name instanceof Horde_Tree_Renderer_Base) {
|
|
$tree = $name;
|
|
$parent = $opts['parent'];
|
|
} else {
|
|
$tree = $injector->getInstance('Horde_Core_Factory_Tree')->create($name, $opts['render_type'], array_merge(array(
|
|
'alternate' => true,
|
|
'lines' => true,
|
|
'lines_base' => true,
|
|
'nosession' => true
|
|
), $opts['render_params']));
|
|
$parent = null;
|
|
}
|
|
|
|
$iterator = empty($opts['iterator'])
|
|
? new IMP_Ftree_IteratorFilter($this)
|
|
: $opts['iterator'];
|
|
|
|
foreach ($iterator as $val) {
|
|
$after = '';
|
|
$elt_parent = null;
|
|
$mbox_ob = $val->mbox_ob;
|
|
$params = array();
|
|
|
|
switch ($opts['render_type']) {
|
|
case 'IMP_Tree_Flist':
|
|
if ($mbox_ob->vfolder_container) {
|
|
continue 2;
|
|
}
|
|
|
|
$is_open = true;
|
|
$label = $params['orig_label'] = empty($opts['basename'])
|
|
? $mbox_ob->abbrev_label
|
|
: $mbox_ob->basename;
|
|
break;
|
|
|
|
case 'IMP_Tree_Jquerymobile':
|
|
$is_open = true;
|
|
$label = $mbox_ob->display_html;
|
|
$icon = $mbox_ob->icon;
|
|
$params['icon'] = $icon->icon;
|
|
$params['special'] = $mbox_ob->inbox || $mbox_ob->special;
|
|
$params['class'] = 'imp-folder';
|
|
$params['urlattributes'] = array(
|
|
'id' => 'imp-mailbox-' . $mbox_ob->form_to
|
|
);
|
|
|
|
/* Force to flat tree so that non-polled parents don't cause
|
|
* polled children to be skipped by renderer (see Bug
|
|
* #11238). */
|
|
$elt_parent = $this[self::BASE_ELT];
|
|
break;
|
|
|
|
case 'IMP_Tree_Simplehtml':
|
|
$is_open = $val->open;
|
|
if ($tree->shouldToggle($mbox_ob->form_to)) {
|
|
if ($is_open) {
|
|
$this->collapse($val);
|
|
} else {
|
|
$this->expand($val);
|
|
}
|
|
$is_open = !$is_open;
|
|
}
|
|
$label = htmlspecialchars(Horde_String::abbreviate($mbox_ob->abbrev_label, 30 - ($val->level * 2)));
|
|
break;
|
|
|
|
case 'Javascript':
|
|
$is_open = $val->open;
|
|
$label = empty($opts['basename'])
|
|
? htmlspecialchars($mbox_ob->abbrev_label)
|
|
: htmlspecialchars($mbox_ob->basename);
|
|
$icon = $mbox_ob->icon;
|
|
$params['icon'] = $icon->icon;
|
|
$params['iconopen'] = $icon->iconopen;
|
|
break;
|
|
}
|
|
|
|
if (!empty($opts['poll_info']) && $val->polled) {
|
|
$poll_info = $mbox_ob->poll_info;
|
|
|
|
if ($poll_info->unseen) {
|
|
switch ($opts['render_type']) {
|
|
case 'IMP_Tree_Jquerymobile':
|
|
$after = $poll_info->unseen;
|
|
break;
|
|
|
|
default:
|
|
$label = '<strong>' . $label . '</strong> (' .
|
|
$poll_info->unseen . ')';
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($val->container) {
|
|
$params['container'] = true;
|
|
} else {
|
|
switch ($view) {
|
|
case $registry::VIEW_MINIMAL:
|
|
$params['url'] = IMP_Minimal_Mailbox::url(array('mailbox' => $mbox_ob));
|
|
break;
|
|
|
|
case $registry::VIEW_SMARTMOBILE:
|
|
$url = new Horde_Core_Smartmobile_Url();
|
|
$url->add('mbox', $mbox_ob->form_to);
|
|
$url->setAnchor('mailbox');
|
|
$params['url'] = strval($url);
|
|
break;
|
|
|
|
default:
|
|
$params['url'] = $mbox_ob->url('mailbox')->setRaw(true);
|
|
break;
|
|
}
|
|
|
|
if (!$val->subscribed) {
|
|
$params['class'] = 'mboxunsub';
|
|
}
|
|
}
|
|
|
|
$checkbox = empty($opts['checkbox'])
|
|
? ''
|
|
: '<input type="checkbox" class="checkbox" name="mbox_list[]" value="' . $mbox_ob->form_to . '"';
|
|
|
|
if ($val->nonimap) {
|
|
$checkbox .= ' disabled="disabled"';
|
|
}
|
|
|
|
if ($val->vfolder &&
|
|
!empty($opts['editvfolder']) &&
|
|
$val->container) {
|
|
$after = ' [' .
|
|
$registry->getServiceLink('prefs', 'imp')->add('group', 'searches')->link(array('title' => _("Edit Virtual Folder"))) . _("Edit") . '</a>'.
|
|
']';
|
|
}
|
|
|
|
if (is_null($elt_parent)) {
|
|
$elt_parent = $val->parent;
|
|
}
|
|
|
|
$tree->addNode(array(
|
|
'id' => $mbox_ob->form_to,
|
|
'parent' => $elt_parent->base_elt ? $parent : $elt_parent->mbox_ob->form_to,
|
|
'label' => $label,
|
|
'expanded' => isset($opts['open']) ? $opts['open'] : $is_open,
|
|
'params' => $params,
|
|
'right' => $after,
|
|
'left' => empty($opts['checkbox']) ? null : $checkbox . ' />'
|
|
));
|
|
}
|
|
|
|
return $tree;
|
|
}
|
|
|
|
/* IteratorAggregate methods. */
|
|
|
|
/**
|
|
* This returns a RecursiveIterator - a RecursiveIteratorIterator is
|
|
* needed to properly iterate through all elements.
|
|
*
|
|
* @return IMP_Ftree_Iterator Iterator object.
|
|
*/
|
|
public function getIterator()
|
|
{
|
|
return new IMP_Ftree_Iterator($this[self::BASE_ELT]);
|
|
}
|
|
|
|
}
|