Objects are now capitalized
authorBen Klang <ben@alkaloid.net>
Sun, 20 Dec 2009 21:31:25 +0000 (21:31 +0000)
committerBen Klang <ben@alkaloid.net>
Sun, 20 Dec 2009 21:31:25 +0000 (21:31 +0000)
git-svn-id: https://svn.alkaloid.net/gpl/shout/trunk@491 06cd67b6-e706-0410-b29e-9de616bca6e9

lib/Driver/Ldap.php [new file with mode: 0644]
lib/Driver/ldap.php [deleted file]

diff --git a/lib/Driver/Ldap.php b/lib/Driver/Ldap.php
new file mode 100644 (file)
index 0000000..8dfe2a8
--- /dev/null
@@ -0,0 +1,932 @@
+<?php
+
+# Make Shout methods available
+require_once SHOUT_BASE . '/lib/Shout.php';
+
+class Shout_Driver_ldap extends Shout_Driver
+{
+    var $_ldapKey;  // Index used for storing objects
+    var $_appKey;   // Index used for moving info to/from the app
+
+    /**
+     * Handle for the current database connection.
+     * @var object LDAP $_LDAP
+     */
+    var $_LDAP;
+
+    /**
+     * Boolean indicating whether or not we're connected to the LDAP
+     * server.
+     * @var boolean $_connected
+     */
+    var $_connected = false;
+
+
+    /**
+    * Constructs a new Shout LDAP driver object.
+    *
+    * @param array  $params    A hash containing connection parameters.
+    */
+    function Shout_Driver_ldap($params = array())
+    {
+        parent::Shout_Driver($params);
+        $this->_connect();
+
+        /* These next lines will translate between indexes used in the
+         * application and LDAP.  The rationale is that translation here will
+         * help make Congregation more driver-independant.  The keys used to
+         * contruct user arrays should be more appropriate to human-legibility
+         * (name instead of 'cn' and email instead of 'mail').  This translation
+         * is only needed because LDAP indexes users based on an arbitrary
+         * attribute and the application indexes by extension/context.  In my
+         * environment users are indexed by their 'mail' attribute and others
+         * may index based on 'cn' or 'uid'.  Any time a new $prefs['uid'] needs
+         * to be supported, this function should be checked and possibly
+         * extended to handle that translation.
+         */
+        switch($this->_params['uid']) {
+        case 'cn':
+            $this->_ldapKey = 'cn';
+            $this->_appKey = 'name';
+            break;
+        case 'mail':
+            $this->_ldapKey = 'mail';
+            $this->_appKey = 'email';
+            break;
+        case 'uid':
+            # FIXME Probably a better app key to map here
+            # There is no value that maps uid to LDAP so we can choose to use
+            # either extension or name, or anything really.  I want to
+            # support it since it's a very common DN attribute.
+            # Since it's entirely administrator's preference, I'll
+            # set it to name for now
+            $this->_ldapKey = 'uid';
+            $this->_appKey = 'name';
+            break;
+        case 'voiceMailbox':
+            $this->_ldapKey = 'voiceMailbox';
+            $this->_appKey = 'extension';
+            break;
+        }
+    }
+
+    /**
+    * Get a list of contexts from the backend
+    *
+    * @param string $filter Search filter
+    *
+    * @return array Contexts valid for this system
+    *
+    * @access private
+    */
+    function &getContexts($searchfilters = SHOUT_CONTEXT_ALL,
+                         $filterperms = null)
+    {
+        static $entries = array();
+        if (isset($entries[$searchfilters])) {
+            return $entries[$searchfilters];
+        }
+
+        if ($filterperms === null) {
+            $filterperms = PERMS_SHOW|PERMS_READ;
+        }
+
+        # TODO Add caching mechanism here.  Possibly cache results per
+        # filter $this->contexts['customer'] and return either data
+        # or possibly a reference to that data
+
+        # Determine which combination of contexts need to be returned
+        if ($searchfilters == SHOUT_CONTEXT_ALL) {
+            $searchfilter="(objectClass=asteriskObject)";
+        } else {
+            $searchfilter = "(&";
+            # FIXME Change this to non-V-Office specific objectClass
+            if ($searchfilters & SHOUT_CONTEXT_CUSTOMERS) {
+                # FIXME what does this objectClass really do for us?
+                $searchfilter.="(objectClass=vofficeCustomer)";
+            }
+            if ($searchfilters & SHOUT_CONTEXT_EXTENSIONS) {
+                $searchfilter.="(objectClass=asteriskExtensions)";
+            }
+            if ($searchfilters & SHOUT_CONTEXT_MOH) {
+                $searchfilter.="(objectClass=asteriskMusicOnHold)";
+            }
+            if ($searchfilters & SHOUT_CONTEXT_CONFERENCE) {
+                $searchfilter.="(objectClass=asteriskMeetMe)";
+            }
+            $searchfilter .= ")";
+        }
+
+        $attributes = array(SHOUT_ACCOUNT_ID_ATTRIBUTE, 'objectClass', 'context');
+
+        # Collect all the possible contexts from the backend
+        $res = @ldap_search($this->_LDAP,
+            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
+            "$searchfilter");
+            #array('context', 'associatedDomain'));
+        if (!$res) {
+            return PEAR::raiseError("Unable to locate any contexts " .
+            "underneath ".SHOUT_ASTERISK_BRANCH.",".$this->_params['basedn'] .
+            " matching those search filters" . ldap_error($this->_LDAP));
+        }
+
+        $res = ldap_get_entries($this->_LDAP, $res);
+        $i = 0;
+        $entries[$searchfilters] = array();
+        while ($i < $res['count']) {
+            $context = $res[$i]['context'][0];
+            $type = SHOUT_CONTEXT_NONE;
+            foreach ($res[$i][strtolower('objectClass')] as $objectClass) {
+                switch ($objectClass) {
+                    case SHOUT_CONTEXT_CUSTOMERS_OBJECTCLASS:
+                        # FIXME What does this objectClass really get us?
+                        $type = $type | SHOUT_CONTEXT_CUSTOMERS;
+                        break;
+                    case SHOUT_CONTEXT_EXTENSIONS_OBJECTCLASS:
+                        $type = $type | SHOUT_CONTEXT_EXTENSIONS;
+                        break;
+                    case SHOUT_CONTEXT_MOH_OBJECTCLASS:
+                        $type = $type | SHOUT_CONTEXT_MOH;
+                        break;
+                    case SHOUT_CONTEXT_CONFERENCE_OBJECTCLASS:
+                        $type = $type | SHOUT_CONTEXT_CONFERENCE;
+                        break;
+                    case SHOUT_CONTEXT_VOICEMAIL_OBJECTCLASS:
+                        $type = $type | SHOUT_CONTEXT_VOICEMAIL;
+                        break;
+                }
+            }
+            if (Shout::checkRights("shout:contexts:$context", $filterperms)) {
+                $entries[$searchfilters][$context] =
+                    array(
+                        'custid' => SHOUT_ACCOUNT_ID_ATTRIBUTE,
+                        'type' => $type,
+                    );
+            }
+            $i++;
+        }
+        # return the array
+        return $entries[$searchfilters];
+    }
+
+    /**
+     * For the given context and type, make sure the context has the
+     * appropriate properties, that it is effectively of that "type"
+     *
+     * @param string $context the context to check type for
+     *
+     * @param string $type the type to verify the context is of
+     *
+     * @return boolean true of the context is of type, false if not
+     *
+     * @access public
+     */
+    function checkContextType($context, $type) {
+        switch ($type) {
+            case "users":
+                $searchfilter = '(objectClass='.SHOUT_CONTEXT_VOICEMAIL_OBJECTCLASS.')';
+                break;
+            case "dialplan":
+                $searchfilter = '(objectClass='.SHOUT_CONTEXT_EXTENSIONS_OBJECTCLASS.')';
+                break;
+            case "moh":
+                $searchfilter='(objectClass='.SHOUT_CONTEXT_MOH_OBJECTCLASS.')';
+                break;
+            case "conference":
+                $searchfilter='(objectClass='.SHOUT_CONTEXT_CONFERENCE_OBJECTCLASS.')';
+                break;
+            case "all":
+            default:
+                $searchfilter="";
+                break;
+        }
+
+        $res = @ldap_search($this->_LDAP,
+            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
+            "(&(objectClass=asteriskObject)$searchfilter(context=$context))",
+            array("context"));
+        if (!$res) {
+            return PEAR::raiseError("Unable to search directory for context
+type");
+        }
+
+        $res = ldap_get_entries($this->_LDAP, $res);
+        if (!$res) {
+            return PEAR::raiseError("Unable to get results from LDAP query");
+        }
+
+        if ($res['count'] == 1) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Get a list of users valid for the contexts
+     *
+     * @param string $context Context on which to search
+     *
+     * @return array User information indexed by voice mailbox number
+     */
+    function getUsers($context)
+    {
+
+        static $entries = array();
+        if (isset($entries[$context])) {
+            return $entries[$context];
+        }
+
+        $basedn = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
+
+        $filter  = '(&';
+        $filter .= '(objectClass='.SHOUT_USER_OBJECTCLASS.')';
+        $filter .= '(context='.$context.')';
+        $filter .= ')';
+
+        $attributes = array(
+            'voiceMailbox',
+            'asteriskUserDialOptions',
+            'asteriskVoiceMailboxOptions',
+            'voiceMailboxPin',
+            'cn',
+            'telephoneNumber',
+            'asteriskUserDialTimeout',
+            'mail',
+            'asteriskPager',
+        );
+
+        $search = @ldap_search($this->_LDAP, $basedn, $filter, $attributes);
+
+        if (!$search) {
+            return PEAR::raiseError("Unable to search directory: " .
+                ldap_error($this->_LDAP));
+        }
+
+        $res = ldap_get_entries($this->_LDAP, $search);
+        #
+        # ATTRIBUTES RETURNED FROM ldap_get_entries ARE ALL LOWER CASE!!
+        #
+        $entries[$context] = array();
+        $i = 0;
+        while ($i < $res['count']) {
+            $extension = $res[$i]['voicemailbox'][0];
+            $uid = md5($res[$i]['dn']);
+            $entries[$context][$extension] = array();
+            $entries[$context][$extension]['uid'] = $uid;
+
+            $j = 0;
+            $entries[$context][$extension]['dialopts'] = array();
+            while ($j < @$res[$i]['asteriskuserdialoptions']['count']) {
+                $entries[$context][$extension]['dialopts'][] =
+                    $res[$i]['asteriskuserdialoptions'][$j];
+                $j++;
+            }
+
+            $j = 0;
+            $entries[$context][$extension]['mailboxopts'] = array();
+            while ($j < @$res[$i]['asteriskvoicemailboxoptions']['count']) {
+                $entries[$context][$extension]['mailboxopts'][] =
+                    $res[$i]['asteriskvoicemailboxoptions'][$j];
+                $j++;
+            }
+
+            $entries[$context][$extension]['mailboxpin'] =
+                $res[$i]['voicemailboxpin'][0];
+
+            @$entries[$context][$extension]['name'] =
+                $res[$i]['cn'][0];
+
+            $j = 0;
+            $entries[$context][$extension]['telephonenumber'] = array();
+            while ($j < @$res[$i]['telephonenumber']['count']) {
+                // Start with 1 for telephone numbers for user convenience
+                $entries[$context][$extension]['telephonenumber'][$j+1] =
+                    $res[$i]['telephonenumber'][$j];
+                $j++;
+            }
+
+            # FIXME Do some sanity checking here.  Also set a default?
+            @$entries[$context][$extension]['dialtimeout'] =
+                $res[$i]['asteriskuserdialtimeout'][0];
+
+            @$entries[$context][$extension]['email'] =
+                $res[$i]['mail'][0];
+
+            @$entries[$context][$extension]['pageremail'] =
+                $res[$i]['asteriskpager'][0];
+
+            $i++;
+
+        }
+
+        ksort($entries[$context]);
+
+        return($entries[$context]);
+    }
+
+    /**
+     * Returns the name of the user's default context
+     *
+     * @return string User's default context
+     */
+    function getHomeContext()
+    {
+        # FIXME Cache this lookup?
+
+        $basedn = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
+        $filter  = '(&';
+        $filter .= '(mail='.Auth::getAuth().')';
+        $filter .= '(objectClass='.SHOUT_USER_OBJECTCLASS.')';
+        $filter .= ')';
+        $attributes = array('context');
+
+        $res = @ldap_search($this->_LDAP, $basedn, $filter, $attributes);
+        if (!$res) {
+            return PEAR::raiseError("Unable to locate any customers " .
+            "underneath ".SHOUT_ASTERISK_BRANCH.",".$this->_params['basedn'] .
+            " matching those search filters");
+            # FIXME Better error string above
+        }
+
+        $res = ldap_get_entries($this->_LDAP, $res);
+
+        # Assume the user only has one context.  The schema enforces this
+        # FIXME: Handle cases where the managing user isn't a valid telephone
+        # system user
+        # FIXME: Do we want to warn?  If so, how?  This PEAR::Error shows up
+        # in unfavorable places (ie. perms screen)
+        if ($res['count'] != 1) {
+            //return PEAR::raiseError(_("Unable to determine default context"));
+            return '';
+        }
+        return $res[0]['context'][0];
+    }
+
+    /**
+     * Get a context's properties
+     *
+     * @param string $context Context to get properties for
+     *
+     * @return integer Bitfield of properties valid for this context
+     */
+    function getContextProperties($context)
+    {
+
+        $res = @ldap_search($this->_LDAP,
+            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
+            "(&(objectClass=asteriskObject)(context=$context))",
+            array('objectClass'));
+        if(!$res) {
+            return PEAR::raiseError(_("Unable to get properties for $context"));
+        }
+
+        $res = ldap_get_entries($this->_LDAP, $res);
+
+        $properties = 0;
+        if ($res['count'] != 1) {
+            return PEAR::raiseError(_("Incorrect number of properties found
+for $context"));
+        }
+
+        foreach ($res[0]['objectclass'] as $objectClass) {
+            switch ($objectClass) {
+                case "vofficeCustomer":
+                    # FIXME What does this objectClass really do for us?
+                    $properties = $properties | SHOUT_CONTEXT_CUSTOMERS;
+                    break;
+
+                case "asteriskExtensions":
+                    $properties = $properties | SHOUT_CONTEXT_EXTENSIONS;
+                    break;
+
+                case "asteriskMusicOnHold":
+                    $properties = $properties | SHOUT_CONTEXT_MOH;
+                    break;
+
+                case "asteriskMeetMe":
+                    $properties = $properties | SHOUT_CONTEXT_CONFERENCE;
+                    break;
+            }
+        }
+        return $properties;
+    }
+
+    /**
+     * Get a context's dialplan and return as a multi-dimensional associative
+     * array
+     *
+     * @param string $context Context to return extensions for
+     *
+     * @param boolean $preprocess Parse includes and barelines and add their
+     *                            information into the extensions array
+     *
+     * @return array Multi-dimensional associative array of extensions data
+     *
+     */
+    function &getDialplan($context, $preprocess = false)
+    {
+        # FIXME Implement preprocess functionality.  Don't forget to cache!
+        static $dialplans = array();
+        if (isset($dialplans[$context])) {
+            return $dialplans[$context];
+        }
+
+        $res = @ldap_search($this->_LDAP,
+            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
+            "(&(objectClass=".SHOUT_CONTEXT_EXTENSIONS_OBJECTCLASS.")(context=$context))",
+            array(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE, SHOUT_DIALPLAN_INCLUDE_ATTRIBUTE,
+                SHOUT_DIALPLAN_IGNOREPAT_ATTRIBUTE, 'description',
+                SHOUT_DIALPLAN_BARELINE_ATTRIBUTE));
+        if (!$res) {
+            return PEAR::raiseError("Unable to locate any extensions " .
+            "underneath ".SHOUT_ASTERISK_BRANCH.",".$this->_params['basedn'] .
+            " matching those search filters");
+        }
+
+        $res = ldap_get_entries($this->_LDAP, $res);
+        $dialplans[$context] = array();
+        $dialplans[$context]['name'] = $context;
+        $i = 0;
+        while ($i < $res['count']) {
+            # Handle extension lines
+            if (isset($res[$i][strtolower(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE)])) {
+                $j = 0;
+                while ($j < $res[$i][strtolower(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE)]['count']) {
+                    @$line = $res[$i][strtolower(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE)][$j];
+
+                    # Basic sanity check for length.  FIXME
+                    if (strlen($line) < 5) {
+                        break;
+                    }
+                    # Can't use strtok here because there may be commass in the
+                    # arg string
+
+                    # Get the extension
+                    $token1 = strpos($line, ',');
+                    $token2 = strpos($line, ',', $token1 + 1);
+                    $token3 = strpos($line, '(', $token2 + 1);
+
+                    $extension = substr($line, 0, $token1);
+                    if (!isset($dialplans[$context]['extensions'][$extension])) {
+                        $dialplan[$context]['extensions'][$extension] = array();
+                    }
+                    $token1++;
+                    # Get the priority
+                    $priority = substr($line, $token1, $token2 - $token1);
+                    $dialplans[$context]['extensions'][$extension][$priority] =
+                        array();
+                    $token2++;
+
+                    # Get Application and args
+                    $application = substr($line, $token2, $token3 - $token2);
+
+                    if ($token3) {
+                        $application = substr($line, $token2, $token3 - $token2);
+                        $args = substr($line, $token3);
+                        $args = preg_replace('/^\(/', '', $args);
+                        $args = preg_replace('/\)$/', '', $args);
+                    } else {
+                        # This application must not have any args
+                        $application = substr($line, $token2);
+                        $args = '';
+                    }
+
+                    # Merge all that data into the returning array
+                    $dialplans[$context]['extensions'][$extension][$priority]['application'] =
+                        $application;
+                    $dialplans[$context]['extensions'][$extension][$priority]['args'] =
+                        $args;
+                    $j++;
+                }
+
+                # Sort the extensions data
+                foreach ($dialplans[$context]['extensions'] as
+                    $extension => $data) {
+                    ksort($dialplans[$context]['extensions'][$extension]);
+                }
+                uksort($dialplans[$context]['extensions'],
+                    array(new Shout, "extensort"));
+            }
+            # Handle include lines
+            if (isset($res[$i]['asteriskincludeline'])) {
+                $j = 0;
+                while ($j < $res[$i]['asteriskincludeline']['count']) {
+                    @$line = $res[$i]['asteriskincludeline'][$j];
+                    $dialplans[$context]['includes'][$j] = $line;
+                    $j++;
+                }
+            }
+
+            # Handle ignorepat
+            if (isset($res[$i]['asteriskignorepat'])) {
+                $j = 0;
+                while ($j < $res[$i]['asteriskignorepat']['count']) {
+                    @$line = $res[$i]['asteriskignorepat'][$j];
+                    $dialplans[$context]['ignorepats'][$j] = $line;
+                    $j++;
+                }
+            }
+            # Handle ignorepat
+            if (isset($res[$i]['asteriskextensionbareline'])) {
+                $j = 0;
+                while ($j < $res[$i]['asteriskextensionbareline']['count']) {
+                    @$line = $res[$i]['asteriskextensionbareline'][$j];
+                    $dialplans[$context]['barelines'][$j] = $line;
+                    $j++;
+                }
+            }
+
+            # Increment object
+            $i++;
+        }
+        return $dialplans[$context];
+    }
+
+    /**
+     * Get the limits for the current user, the user's context, and global
+     * Return the most specific values in every case.  Return default values
+     * where no data is found.  If $extension is specified, $context must
+     * also be specified.
+     *
+     * @param optional string $context Context to search
+     *
+     * @param optional string $extension Extension/user to search
+     *
+     * @return array Array with elements indicating various limits
+     */
+     # FIXME Figure out how this fits into Shout/Congregation better
+    function &getLimits($context = null, $extension = null)
+    {
+
+        $limits = array('telephonenumbersmax',
+                        'voicemailboxesmax',
+                        'asteriskusersmax');
+
+        if(!is_null($extension) && is_null($context)) {
+            return PEAR::raiseError("Extension specified but no context " .
+                "given.");
+        }
+
+        if (!is_null($context) && isset($limits[$context])) {
+            if (!is_null($extension) &&
+                isset($limits[$context][$extension])) {
+                return $limits[$context][$extension];
+            }
+            return $limits[$context];
+        }
+
+        # Set some default limits (to unlimited)
+        static $cachedlimits = array();
+        # Initialize the limits with defaults
+        if (count($cachedlimits) < 1) {
+            foreach ($limits as $limit) {
+                $cachedlimits[$limit] = 99999;
+            }
+        }
+
+        # Collect the global limits
+        $res = @ldap_search($this->_LDAP,
+            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
+            '(&(objectClass=asteriskLimits)(cn=globals))',
+            $limits);
+
+        if (!$res) {
+            return PEAR::raiseError('Unable to search the LDAP server for ' .
+                'global limits');
+        }
+
+        $res = ldap_get_entries($this->_LDAP, $res);
+        # There should only have been one object returned so we'll just take the
+        # first result returned
+        if ($res['count'] > 0) {
+            foreach ($limits as $limit) {
+                if (isset($res[0][$limit][0])) {
+                    $cachedlimits[$limit] = $res[0][$limit][0];
+                }
+            }
+        } else {
+            return PEAR::raiseError("No global object found.");
+        }
+
+        # Get limits for the context, if provided
+        if (isset($context)) {
+            $res = ldap_search($this->_LDAP,
+                SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
+                "(&(objectClass=asteriskLimits)(cn=$context))");
+
+            if (!$res) {
+                return PEAR::raiseError('Unable to search the LDAP server ' .
+                    "for $context specific limits");
+            }
+
+            $cachedlimits[$context][$extension] = array();
+            if ($res['count'] > 0) {
+                foreach ($limits as $limit) {
+                    if (isset($res[0][$limit][0])) {
+                        $cachedlimits[$context][$limit] = $res[0][$limit][0];
+                    } else {
+                        # If no value is provided use the global limit
+                        $cachedlimits[$context][$limit] = $cachedlimits[$limit];
+                    }
+                }
+            } else {
+
+                foreach ($limits as $limit) {
+                    $cachedlimits[$context][$limit] =
+                        $cachedlimits[$limit];
+                }
+            }
+
+            if (isset($extension)) {
+                $res = @ldap_search($this->_LDAP,
+                    SHOUT_USERS_BRANCH.','.$this->_params['basedn'],
+                    "(&(objectClass=asteriskLimits)(voiceMailbox=$extension)".
+                    "(context=$context))");
+
+                if (!$res) {
+                    return PEAR::raiseError('Unable to search the LDAP server '.
+                        "for Extension $extension, $context specific limits");
+                }
+
+                $cachedlimits[$context][$extension] = array();
+                if ($res['count'] > 0) {
+                    foreach ($limits as $limit) {
+                        if (isset($res[0][$limit][0])) {
+                            $cachedlimits[$context][$extension][$limit] =
+                                $res[0][$limit][0];
+                        } else {
+                            # If no value is provided use the context limit
+                            $cachedlimits[$context][$extension][$limit] =
+                                $cachedlimits[$context][$limit];
+                        }
+                    }
+                } else {
+                    foreach ($limits as $limit) {
+                        $cachedlimits[$context][$extension][$limit] =
+                            $cachedlimits[$context][$limit];
+                    }
+                }
+                return $cachedlimits[$context][$extension];
+            }
+            return $cachedlimits[$context];
+        }
+    }
+
+    /**
+     * Save a user to the LDAP tree
+     *
+     * @param string $context Context to which the user should be added
+     *
+     * @param string $extension Extension to be saved
+     *
+     * @param array $userdetails Phone numbers, PIN, options, etc to be saved
+     *
+     * @return TRUE on success, PEAR::Error object on error
+     */
+    function saveUser($context, $extension, $userdetails)
+    {
+        $ldapKey = &$this->_ldapKey;
+        $appKey = &$this->_appKey;
+        # FIXME Access Control/Authorization
+        if (
+            !(Shout::checkRights("shout:contexts:$context:users", PERMS_EDIT, 1))
+            &&
+            !($userdetails[$appKey] == Auth::getAuth())
+            ) {
+            return PEAR::raiseError("No permission to modify users in this " .
+                "context.");
+        }
+
+        $contexts = &$this->getContexts();
+//         $domain = $contexts[$context]['domain'];
+
+        # Check to ensure the extension is unique within this context
+        $filter = "(&(objectClass=asteriskVoiceMailbox)(context=$context))";
+        $reqattrs = array('dn', $ldapKey);
+        $res = @ldap_search($this->_LDAP,
+            SHOUT_USERS_BRANCH . ',' . $this->_params['basedn'],
+            $filter, $reqattrs);
+        if (!$res) {
+            return PEAR::raiseError('Unable to check directory for duplicate extension: ' .
+                ldap_error($this->_LDAP));
+        }
+        if (($res['count'] > 1) ||
+            ($res['count'] != 0 &&
+            !in_array($res[0][$ldapKey], $userdetails[$appKey]))) {
+            return PEAR::raiseError('Duplicate extension found.  Not saving changes.');
+        }
+
+        $entry = array(
+            'cn' => $userdetails['name'],
+            'sn' => $userdetails['name'],
+            'mail' => $userdetails['email'],
+            'uid' => $userdetails['email'],
+            'voiceMailbox' => $userdetails['newextension'],
+            'voiceMailboxPin' => $userdetails['mailboxpin'],
+            'context' => $context,
+            'asteriskUserDialOptions' => $userdetails['dialopts'],
+        );
+
+        if (!empty ($userdetails['telephonenumber'])) {
+            $entry['telephoneNumber'] = $userdetails['telephonenumber'];
+        }
+
+        $validusers = &$this->getUsers($context);
+        if (!isset($validusers[$extension])) {
+            # Test to see if we're modifying an existing user that has
+            # no telephone system objectClasses and update that object/user
+            $rdn = $ldapKey.'='.$userdetails[$appKey].',';
+            $branch = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
+
+            # This test is something of a hack.  I want a cheap way to check
+            # for the existance of an object.  I don't want to do a full search
+            # so instead I compare that the dn equals the dn.  If the object
+            # exists then it'll return true.  If the object doesn't exist,
+            # it'll return error.  If it ever returns false something wierd
+            # is going on.
+            $res = @ldap_compare($this->_LDAP, $rdn.$branch,
+                    $ldapKey, $userdetails[$appKey]);
+            if ($res === false) {
+                # We should never get here: a DN should ALWAYS match itself
+                return PEAR::raiseError("Internal Error: " . __FILE__ . " at " .
+                    __LINE__);
+            } elseif ($res === true) {
+                # The object/user exists but doesn't have the Asterisk
+                # objectClasses
+                $extension = $userdetails['newextension'];
+
+                # $tmp is the minimal information required to establish
+                # an account in LDAP as required by the objectClasses.
+                # The entry will be fully populated below.
+                $tmp = array();
+                $tmp['objectClass'] = array(
+                    'asteriskUser',
+                    'asteriskVoiceMailbox'
+                );
+                $tmp['voiceMailbox'] = $extension;
+                $tmp['context'] = $context;
+                $res = @ldap_mod_replace($this->_LDAP, $rdn.$branch, $tmp);
+                if (!$res) {
+                    return PEAR::raiseError("Unable to modify the user: " .
+                        ldap_error($this->_LDAP));
+                }
+
+                # Populate the $validusers array to make the edit go smoothly
+                # below
+                $validusers[$extension] = array();
+                $validusers[$extension][$appKey] = $userdetails[$appKey];
+
+                # The remainder of the work is done at the outside of the
+                # parent if() like a normal edit.
+
+            } elseif ($res === -1) {
+                # We must be adding a new user.
+                $entry['objectClass'] = array(
+                    'top',
+                    'person',
+                    'organizationalPerson',
+                    'inetOrgPerson',
+                    'hordePerson',
+                    'asteriskUser',
+                    'asteriskVoiceMailbox'
+                );
+
+                # Check to see if the maximum number of users for this context
+                # has been reached
+                $limits = $this->getLimits($context);
+                if (is_a($limits, "PEAR_Error")) {
+                    return $limits;
+                }
+                if (count($validusers) >= $limits['asteriskusersmax']) {
+                    return PEAR::raiseError('Maximum number of users reached.');
+                }
+
+                $res = @ldap_add($this->_LDAP, $rdn.$branch, $entry);
+                if (!$res) {
+                    return PEAR::raiseError('LDAP Add failed: ' .
+                        ldap_error($this->_LDAP));
+                }
+
+                return true;
+            } elseif (is_a($res, 'PEAR_Error')) {
+                # Some kind of internal error; not even sure if this is a
+                # possible outcome or not but I'll play it safe.
+                return $res;
+            }
+        }
+
+        # Anything after this point is an edit.
+
+        # Check to see if the object needs to be renamed (DN changed)
+        if ($validusers[$extension][$appKey] != $entry[$ldapKey]) {
+            $oldrdn = $ldapKey.'='.$validusers[$extension][$appKey];
+            $oldparent = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
+            $newrdn = $ldapKey.'='.$entry[$ldapKey];
+            $res = @ldap_rename($this->_LDAP, "$oldrdn,$oldparent",
+                $newrdn, $oldparent, true);
+            if (!$res) {
+                return PEAR::raiseError('LDAP Rename failed: ' .
+                    ldap_error($this->_LDAP));
+            }
+        }
+
+        # Update the object/user
+        $dn = $ldapKey.'='.$entry[$ldapKey];
+        $dn .= ','.SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
+        $res = @ldap_modify($this->_LDAP, $dn, $entry);
+        if (!$res) {
+            return PEAR::raiseError('LDAP Modify failed: ' .
+                ldap_error($this->_LDAP));
+        }
+
+        # We must have been successful
+        return true;
+    }
+
+    /**
+     * Deletes a user from the LDAP tree
+     *
+     * @param string $context Context to delete the user from
+     * @param string $extension Extension of the user to be deleted
+     *
+     * @return boolean True on success, PEAR::Error object on error
+     */
+    function deleteUser($context, $extension)
+    {
+        $ldapKey = &$this->_ldapKey;
+        $appKey = &$this->_appKey;
+
+        if (!Shout::checkRights("shout:contexts:$context:users",
+            PERMS_DELETE, 1)) {
+            return PEAR::raiseError("No permission to delete users in this " .
+                "context.");
+        }
+
+        $validusers = $this->getUsers($context);
+        if (!isset($validusers[$extension])) {
+            return PEAR::raiseError("That extension does not exist.");
+        }
+
+        $dn = "$ldapKey=".$validusers[$extension][$appKey];
+        $dn .= ',' . SHOUT_USERS_BRANCH . ',' . $this->_params['basedn'];
+
+        $res = @ldap_delete($this->_LDAP, $dn);
+        if (!$res) {
+            return PEAR::raiseError("Unable to delete $extension from " .
+                "$context: " . ldap_error($this->_LDAP));
+        }
+        return true;
+    }
+
+
+    /* Needed because uksort can't take a classed function as its callback arg */
+    function _sortexten($e1, $e2)
+    {
+        print "$e1 and $e2\n";
+        $ret =  Shout::extensort($e1, $e2);
+        print "returning $ret";
+        return $ret;
+    }
+
+    /**
+     * Attempts to open a connection to the LDAP server.
+     *
+     * @return boolean    True on success; exits (Horde::fatal()) on error.
+     *
+     * @access private
+     */
+    function _connect()
+    {
+        if (!$this->_connected) {
+            # FIXME What else is needed for this assert?
+            Horde::assertDriverConfig($this->_params, 'storage',
+                array('hostspec', 'basedn', 'binddn', 'password'));
+
+            # FIXME Add other sane defaults here (mostly objectClass related)
+            if (!isset($this->_params['userObjectclass'])) {
+                $this->_params['userObjectclass'] = 'asteriskUser';
+            }
+
+            $this->_LDAP = ldap_connect($this->_params['hostspec'], 389); #FIXME
+            if (!$this->_LDAP) {
+                Horde::fatal("Unable to connect to LDAP server $hostname on
+$port", __FILE__, __LINE__); #FIXME: $port
+            }
+            $res = ldap_set_option($this->_LDAP, LDAP_OPT_PROTOCOL_VERSION,
+$this->_params['version']);
+            if (!$res) {
+                return PEAR::raiseError("Unable to set LDAP protocol version");
+            }
+            $res = ldap_bind($this->_LDAP, $this->_params['binddn'],
+$this->_params['password']);
+            if (!$res) {
+                return PEAR::raiseError("Unable to bind to the LDAP server.
+Check authentication credentials.");
+            }
+
+            $this->_connected = true;
+        }
+        return true;
+    }
+}
diff --git a/lib/Driver/ldap.php b/lib/Driver/ldap.php
deleted file mode 100644 (file)
index 8dfe2a8..0000000
+++ /dev/null
@@ -1,932 +0,0 @@
-<?php
-
-# Make Shout methods available
-require_once SHOUT_BASE . '/lib/Shout.php';
-
-class Shout_Driver_ldap extends Shout_Driver
-{
-    var $_ldapKey;  // Index used for storing objects
-    var $_appKey;   // Index used for moving info to/from the app
-
-    /**
-     * Handle for the current database connection.
-     * @var object LDAP $_LDAP
-     */
-    var $_LDAP;
-
-    /**
-     * Boolean indicating whether or not we're connected to the LDAP
-     * server.
-     * @var boolean $_connected
-     */
-    var $_connected = false;
-
-
-    /**
-    * Constructs a new Shout LDAP driver object.
-    *
-    * @param array  $params    A hash containing connection parameters.
-    */
-    function Shout_Driver_ldap($params = array())
-    {
-        parent::Shout_Driver($params);
-        $this->_connect();
-
-        /* These next lines will translate between indexes used in the
-         * application and LDAP.  The rationale is that translation here will
-         * help make Congregation more driver-independant.  The keys used to
-         * contruct user arrays should be more appropriate to human-legibility
-         * (name instead of 'cn' and email instead of 'mail').  This translation
-         * is only needed because LDAP indexes users based on an arbitrary
-         * attribute and the application indexes by extension/context.  In my
-         * environment users are indexed by their 'mail' attribute and others
-         * may index based on 'cn' or 'uid'.  Any time a new $prefs['uid'] needs
-         * to be supported, this function should be checked and possibly
-         * extended to handle that translation.
-         */
-        switch($this->_params['uid']) {
-        case 'cn':
-            $this->_ldapKey = 'cn';
-            $this->_appKey = 'name';
-            break;
-        case 'mail':
-            $this->_ldapKey = 'mail';
-            $this->_appKey = 'email';
-            break;
-        case 'uid':
-            # FIXME Probably a better app key to map here
-            # There is no value that maps uid to LDAP so we can choose to use
-            # either extension or name, or anything really.  I want to
-            # support it since it's a very common DN attribute.
-            # Since it's entirely administrator's preference, I'll
-            # set it to name for now
-            $this->_ldapKey = 'uid';
-            $this->_appKey = 'name';
-            break;
-        case 'voiceMailbox':
-            $this->_ldapKey = 'voiceMailbox';
-            $this->_appKey = 'extension';
-            break;
-        }
-    }
-
-    /**
-    * Get a list of contexts from the backend
-    *
-    * @param string $filter Search filter
-    *
-    * @return array Contexts valid for this system
-    *
-    * @access private
-    */
-    function &getContexts($searchfilters = SHOUT_CONTEXT_ALL,
-                         $filterperms = null)
-    {
-        static $entries = array();
-        if (isset($entries[$searchfilters])) {
-            return $entries[$searchfilters];
-        }
-
-        if ($filterperms === null) {
-            $filterperms = PERMS_SHOW|PERMS_READ;
-        }
-
-        # TODO Add caching mechanism here.  Possibly cache results per
-        # filter $this->contexts['customer'] and return either data
-        # or possibly a reference to that data
-
-        # Determine which combination of contexts need to be returned
-        if ($searchfilters == SHOUT_CONTEXT_ALL) {
-            $searchfilter="(objectClass=asteriskObject)";
-        } else {
-            $searchfilter = "(&";
-            # FIXME Change this to non-V-Office specific objectClass
-            if ($searchfilters & SHOUT_CONTEXT_CUSTOMERS) {
-                # FIXME what does this objectClass really do for us?
-                $searchfilter.="(objectClass=vofficeCustomer)";
-            }
-            if ($searchfilters & SHOUT_CONTEXT_EXTENSIONS) {
-                $searchfilter.="(objectClass=asteriskExtensions)";
-            }
-            if ($searchfilters & SHOUT_CONTEXT_MOH) {
-                $searchfilter.="(objectClass=asteriskMusicOnHold)";
-            }
-            if ($searchfilters & SHOUT_CONTEXT_CONFERENCE) {
-                $searchfilter.="(objectClass=asteriskMeetMe)";
-            }
-            $searchfilter .= ")";
-        }
-
-        $attributes = array(SHOUT_ACCOUNT_ID_ATTRIBUTE, 'objectClass', 'context');
-
-        # Collect all the possible contexts from the backend
-        $res = @ldap_search($this->_LDAP,
-            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
-            "$searchfilter");
-            #array('context', 'associatedDomain'));
-        if (!$res) {
-            return PEAR::raiseError("Unable to locate any contexts " .
-            "underneath ".SHOUT_ASTERISK_BRANCH.",".$this->_params['basedn'] .
-            " matching those search filters" . ldap_error($this->_LDAP));
-        }
-
-        $res = ldap_get_entries($this->_LDAP, $res);
-        $i = 0;
-        $entries[$searchfilters] = array();
-        while ($i < $res['count']) {
-            $context = $res[$i]['context'][0];
-            $type = SHOUT_CONTEXT_NONE;
-            foreach ($res[$i][strtolower('objectClass')] as $objectClass) {
-                switch ($objectClass) {
-                    case SHOUT_CONTEXT_CUSTOMERS_OBJECTCLASS:
-                        # FIXME What does this objectClass really get us?
-                        $type = $type | SHOUT_CONTEXT_CUSTOMERS;
-                        break;
-                    case SHOUT_CONTEXT_EXTENSIONS_OBJECTCLASS:
-                        $type = $type | SHOUT_CONTEXT_EXTENSIONS;
-                        break;
-                    case SHOUT_CONTEXT_MOH_OBJECTCLASS:
-                        $type = $type | SHOUT_CONTEXT_MOH;
-                        break;
-                    case SHOUT_CONTEXT_CONFERENCE_OBJECTCLASS:
-                        $type = $type | SHOUT_CONTEXT_CONFERENCE;
-                        break;
-                    case SHOUT_CONTEXT_VOICEMAIL_OBJECTCLASS:
-                        $type = $type | SHOUT_CONTEXT_VOICEMAIL;
-                        break;
-                }
-            }
-            if (Shout::checkRights("shout:contexts:$context", $filterperms)) {
-                $entries[$searchfilters][$context] =
-                    array(
-                        'custid' => SHOUT_ACCOUNT_ID_ATTRIBUTE,
-                        'type' => $type,
-                    );
-            }
-            $i++;
-        }
-        # return the array
-        return $entries[$searchfilters];
-    }
-
-    /**
-     * For the given context and type, make sure the context has the
-     * appropriate properties, that it is effectively of that "type"
-     *
-     * @param string $context the context to check type for
-     *
-     * @param string $type the type to verify the context is of
-     *
-     * @return boolean true of the context is of type, false if not
-     *
-     * @access public
-     */
-    function checkContextType($context, $type) {
-        switch ($type) {
-            case "users":
-                $searchfilter = '(objectClass='.SHOUT_CONTEXT_VOICEMAIL_OBJECTCLASS.')';
-                break;
-            case "dialplan":
-                $searchfilter = '(objectClass='.SHOUT_CONTEXT_EXTENSIONS_OBJECTCLASS.')';
-                break;
-            case "moh":
-                $searchfilter='(objectClass='.SHOUT_CONTEXT_MOH_OBJECTCLASS.')';
-                break;
-            case "conference":
-                $searchfilter='(objectClass='.SHOUT_CONTEXT_CONFERENCE_OBJECTCLASS.')';
-                break;
-            case "all":
-            default:
-                $searchfilter="";
-                break;
-        }
-
-        $res = @ldap_search($this->_LDAP,
-            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
-            "(&(objectClass=asteriskObject)$searchfilter(context=$context))",
-            array("context"));
-        if (!$res) {
-            return PEAR::raiseError("Unable to search directory for context
-type");
-        }
-
-        $res = ldap_get_entries($this->_LDAP, $res);
-        if (!$res) {
-            return PEAR::raiseError("Unable to get results from LDAP query");
-        }
-
-        if ($res['count'] == 1) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Get a list of users valid for the contexts
-     *
-     * @param string $context Context on which to search
-     *
-     * @return array User information indexed by voice mailbox number
-     */
-    function getUsers($context)
-    {
-
-        static $entries = array();
-        if (isset($entries[$context])) {
-            return $entries[$context];
-        }
-
-        $basedn = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
-
-        $filter  = '(&';
-        $filter .= '(objectClass='.SHOUT_USER_OBJECTCLASS.')';
-        $filter .= '(context='.$context.')';
-        $filter .= ')';
-
-        $attributes = array(
-            'voiceMailbox',
-            'asteriskUserDialOptions',
-            'asteriskVoiceMailboxOptions',
-            'voiceMailboxPin',
-            'cn',
-            'telephoneNumber',
-            'asteriskUserDialTimeout',
-            'mail',
-            'asteriskPager',
-        );
-
-        $search = @ldap_search($this->_LDAP, $basedn, $filter, $attributes);
-
-        if (!$search) {
-            return PEAR::raiseError("Unable to search directory: " .
-                ldap_error($this->_LDAP));
-        }
-
-        $res = ldap_get_entries($this->_LDAP, $search);
-        #
-        # ATTRIBUTES RETURNED FROM ldap_get_entries ARE ALL LOWER CASE!!
-        #
-        $entries[$context] = array();
-        $i = 0;
-        while ($i < $res['count']) {
-            $extension = $res[$i]['voicemailbox'][0];
-            $uid = md5($res[$i]['dn']);
-            $entries[$context][$extension] = array();
-            $entries[$context][$extension]['uid'] = $uid;
-
-            $j = 0;
-            $entries[$context][$extension]['dialopts'] = array();
-            while ($j < @$res[$i]['asteriskuserdialoptions']['count']) {
-                $entries[$context][$extension]['dialopts'][] =
-                    $res[$i]['asteriskuserdialoptions'][$j];
-                $j++;
-            }
-
-            $j = 0;
-            $entries[$context][$extension]['mailboxopts'] = array();
-            while ($j < @$res[$i]['asteriskvoicemailboxoptions']['count']) {
-                $entries[$context][$extension]['mailboxopts'][] =
-                    $res[$i]['asteriskvoicemailboxoptions'][$j];
-                $j++;
-            }
-
-            $entries[$context][$extension]['mailboxpin'] =
-                $res[$i]['voicemailboxpin'][0];
-
-            @$entries[$context][$extension]['name'] =
-                $res[$i]['cn'][0];
-
-            $j = 0;
-            $entries[$context][$extension]['telephonenumber'] = array();
-            while ($j < @$res[$i]['telephonenumber']['count']) {
-                // Start with 1 for telephone numbers for user convenience
-                $entries[$context][$extension]['telephonenumber'][$j+1] =
-                    $res[$i]['telephonenumber'][$j];
-                $j++;
-            }
-
-            # FIXME Do some sanity checking here.  Also set a default?
-            @$entries[$context][$extension]['dialtimeout'] =
-                $res[$i]['asteriskuserdialtimeout'][0];
-
-            @$entries[$context][$extension]['email'] =
-                $res[$i]['mail'][0];
-
-            @$entries[$context][$extension]['pageremail'] =
-                $res[$i]['asteriskpager'][0];
-
-            $i++;
-
-        }
-
-        ksort($entries[$context]);
-
-        return($entries[$context]);
-    }
-
-    /**
-     * Returns the name of the user's default context
-     *
-     * @return string User's default context
-     */
-    function getHomeContext()
-    {
-        # FIXME Cache this lookup?
-
-        $basedn = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
-        $filter  = '(&';
-        $filter .= '(mail='.Auth::getAuth().')';
-        $filter .= '(objectClass='.SHOUT_USER_OBJECTCLASS.')';
-        $filter .= ')';
-        $attributes = array('context');
-
-        $res = @ldap_search($this->_LDAP, $basedn, $filter, $attributes);
-        if (!$res) {
-            return PEAR::raiseError("Unable to locate any customers " .
-            "underneath ".SHOUT_ASTERISK_BRANCH.",".$this->_params['basedn'] .
-            " matching those search filters");
-            # FIXME Better error string above
-        }
-
-        $res = ldap_get_entries($this->_LDAP, $res);
-
-        # Assume the user only has one context.  The schema enforces this
-        # FIXME: Handle cases where the managing user isn't a valid telephone
-        # system user
-        # FIXME: Do we want to warn?  If so, how?  This PEAR::Error shows up
-        # in unfavorable places (ie. perms screen)
-        if ($res['count'] != 1) {
-            //return PEAR::raiseError(_("Unable to determine default context"));
-            return '';
-        }
-        return $res[0]['context'][0];
-    }
-
-    /**
-     * Get a context's properties
-     *
-     * @param string $context Context to get properties for
-     *
-     * @return integer Bitfield of properties valid for this context
-     */
-    function getContextProperties($context)
-    {
-
-        $res = @ldap_search($this->_LDAP,
-            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
-            "(&(objectClass=asteriskObject)(context=$context))",
-            array('objectClass'));
-        if(!$res) {
-            return PEAR::raiseError(_("Unable to get properties for $context"));
-        }
-
-        $res = ldap_get_entries($this->_LDAP, $res);
-
-        $properties = 0;
-        if ($res['count'] != 1) {
-            return PEAR::raiseError(_("Incorrect number of properties found
-for $context"));
-        }
-
-        foreach ($res[0]['objectclass'] as $objectClass) {
-            switch ($objectClass) {
-                case "vofficeCustomer":
-                    # FIXME What does this objectClass really do for us?
-                    $properties = $properties | SHOUT_CONTEXT_CUSTOMERS;
-                    break;
-
-                case "asteriskExtensions":
-                    $properties = $properties | SHOUT_CONTEXT_EXTENSIONS;
-                    break;
-
-                case "asteriskMusicOnHold":
-                    $properties = $properties | SHOUT_CONTEXT_MOH;
-                    break;
-
-                case "asteriskMeetMe":
-                    $properties = $properties | SHOUT_CONTEXT_CONFERENCE;
-                    break;
-            }
-        }
-        return $properties;
-    }
-
-    /**
-     * Get a context's dialplan and return as a multi-dimensional associative
-     * array
-     *
-     * @param string $context Context to return extensions for
-     *
-     * @param boolean $preprocess Parse includes and barelines and add their
-     *                            information into the extensions array
-     *
-     * @return array Multi-dimensional associative array of extensions data
-     *
-     */
-    function &getDialplan($context, $preprocess = false)
-    {
-        # FIXME Implement preprocess functionality.  Don't forget to cache!
-        static $dialplans = array();
-        if (isset($dialplans[$context])) {
-            return $dialplans[$context];
-        }
-
-        $res = @ldap_search($this->_LDAP,
-            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
-            "(&(objectClass=".SHOUT_CONTEXT_EXTENSIONS_OBJECTCLASS.")(context=$context))",
-            array(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE, SHOUT_DIALPLAN_INCLUDE_ATTRIBUTE,
-                SHOUT_DIALPLAN_IGNOREPAT_ATTRIBUTE, 'description',
-                SHOUT_DIALPLAN_BARELINE_ATTRIBUTE));
-        if (!$res) {
-            return PEAR::raiseError("Unable to locate any extensions " .
-            "underneath ".SHOUT_ASTERISK_BRANCH.",".$this->_params['basedn'] .
-            " matching those search filters");
-        }
-
-        $res = ldap_get_entries($this->_LDAP, $res);
-        $dialplans[$context] = array();
-        $dialplans[$context]['name'] = $context;
-        $i = 0;
-        while ($i < $res['count']) {
-            # Handle extension lines
-            if (isset($res[$i][strtolower(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE)])) {
-                $j = 0;
-                while ($j < $res[$i][strtolower(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE)]['count']) {
-                    @$line = $res[$i][strtolower(SHOUT_DIALPLAN_EXTENSIONLINE_ATTRIBUTE)][$j];
-
-                    # Basic sanity check for length.  FIXME
-                    if (strlen($line) < 5) {
-                        break;
-                    }
-                    # Can't use strtok here because there may be commass in the
-                    # arg string
-
-                    # Get the extension
-                    $token1 = strpos($line, ',');
-                    $token2 = strpos($line, ',', $token1 + 1);
-                    $token3 = strpos($line, '(', $token2 + 1);
-
-                    $extension = substr($line, 0, $token1);
-                    if (!isset($dialplans[$context]['extensions'][$extension])) {
-                        $dialplan[$context]['extensions'][$extension] = array();
-                    }
-                    $token1++;
-                    # Get the priority
-                    $priority = substr($line, $token1, $token2 - $token1);
-                    $dialplans[$context]['extensions'][$extension][$priority] =
-                        array();
-                    $token2++;
-
-                    # Get Application and args
-                    $application = substr($line, $token2, $token3 - $token2);
-
-                    if ($token3) {
-                        $application = substr($line, $token2, $token3 - $token2);
-                        $args = substr($line, $token3);
-                        $args = preg_replace('/^\(/', '', $args);
-                        $args = preg_replace('/\)$/', '', $args);
-                    } else {
-                        # This application must not have any args
-                        $application = substr($line, $token2);
-                        $args = '';
-                    }
-
-                    # Merge all that data into the returning array
-                    $dialplans[$context]['extensions'][$extension][$priority]['application'] =
-                        $application;
-                    $dialplans[$context]['extensions'][$extension][$priority]['args'] =
-                        $args;
-                    $j++;
-                }
-
-                # Sort the extensions data
-                foreach ($dialplans[$context]['extensions'] as
-                    $extension => $data) {
-                    ksort($dialplans[$context]['extensions'][$extension]);
-                }
-                uksort($dialplans[$context]['extensions'],
-                    array(new Shout, "extensort"));
-            }
-            # Handle include lines
-            if (isset($res[$i]['asteriskincludeline'])) {
-                $j = 0;
-                while ($j < $res[$i]['asteriskincludeline']['count']) {
-                    @$line = $res[$i]['asteriskincludeline'][$j];
-                    $dialplans[$context]['includes'][$j] = $line;
-                    $j++;
-                }
-            }
-
-            # Handle ignorepat
-            if (isset($res[$i]['asteriskignorepat'])) {
-                $j = 0;
-                while ($j < $res[$i]['asteriskignorepat']['count']) {
-                    @$line = $res[$i]['asteriskignorepat'][$j];
-                    $dialplans[$context]['ignorepats'][$j] = $line;
-                    $j++;
-                }
-            }
-            # Handle ignorepat
-            if (isset($res[$i]['asteriskextensionbareline'])) {
-                $j = 0;
-                while ($j < $res[$i]['asteriskextensionbareline']['count']) {
-                    @$line = $res[$i]['asteriskextensionbareline'][$j];
-                    $dialplans[$context]['barelines'][$j] = $line;
-                    $j++;
-                }
-            }
-
-            # Increment object
-            $i++;
-        }
-        return $dialplans[$context];
-    }
-
-    /**
-     * Get the limits for the current user, the user's context, and global
-     * Return the most specific values in every case.  Return default values
-     * where no data is found.  If $extension is specified, $context must
-     * also be specified.
-     *
-     * @param optional string $context Context to search
-     *
-     * @param optional string $extension Extension/user to search
-     *
-     * @return array Array with elements indicating various limits
-     */
-     # FIXME Figure out how this fits into Shout/Congregation better
-    function &getLimits($context = null, $extension = null)
-    {
-
-        $limits = array('telephonenumbersmax',
-                        'voicemailboxesmax',
-                        'asteriskusersmax');
-
-        if(!is_null($extension) && is_null($context)) {
-            return PEAR::raiseError("Extension specified but no context " .
-                "given.");
-        }
-
-        if (!is_null($context) && isset($limits[$context])) {
-            if (!is_null($extension) &&
-                isset($limits[$context][$extension])) {
-                return $limits[$context][$extension];
-            }
-            return $limits[$context];
-        }
-
-        # Set some default limits (to unlimited)
-        static $cachedlimits = array();
-        # Initialize the limits with defaults
-        if (count($cachedlimits) < 1) {
-            foreach ($limits as $limit) {
-                $cachedlimits[$limit] = 99999;
-            }
-        }
-
-        # Collect the global limits
-        $res = @ldap_search($this->_LDAP,
-            SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
-            '(&(objectClass=asteriskLimits)(cn=globals))',
-            $limits);
-
-        if (!$res) {
-            return PEAR::raiseError('Unable to search the LDAP server for ' .
-                'global limits');
-        }
-
-        $res = ldap_get_entries($this->_LDAP, $res);
-        # There should only have been one object returned so we'll just take the
-        # first result returned
-        if ($res['count'] > 0) {
-            foreach ($limits as $limit) {
-                if (isset($res[0][$limit][0])) {
-                    $cachedlimits[$limit] = $res[0][$limit][0];
-                }
-            }
-        } else {
-            return PEAR::raiseError("No global object found.");
-        }
-
-        # Get limits for the context, if provided
-        if (isset($context)) {
-            $res = ldap_search($this->_LDAP,
-                SHOUT_ASTERISK_BRANCH.','.$this->_params['basedn'],
-                "(&(objectClass=asteriskLimits)(cn=$context))");
-
-            if (!$res) {
-                return PEAR::raiseError('Unable to search the LDAP server ' .
-                    "for $context specific limits");
-            }
-
-            $cachedlimits[$context][$extension] = array();
-            if ($res['count'] > 0) {
-                foreach ($limits as $limit) {
-                    if (isset($res[0][$limit][0])) {
-                        $cachedlimits[$context][$limit] = $res[0][$limit][0];
-                    } else {
-                        # If no value is provided use the global limit
-                        $cachedlimits[$context][$limit] = $cachedlimits[$limit];
-                    }
-                }
-            } else {
-
-                foreach ($limits as $limit) {
-                    $cachedlimits[$context][$limit] =
-                        $cachedlimits[$limit];
-                }
-            }
-
-            if (isset($extension)) {
-                $res = @ldap_search($this->_LDAP,
-                    SHOUT_USERS_BRANCH.','.$this->_params['basedn'],
-                    "(&(objectClass=asteriskLimits)(voiceMailbox=$extension)".
-                    "(context=$context))");
-
-                if (!$res) {
-                    return PEAR::raiseError('Unable to search the LDAP server '.
-                        "for Extension $extension, $context specific limits");
-                }
-
-                $cachedlimits[$context][$extension] = array();
-                if ($res['count'] > 0) {
-                    foreach ($limits as $limit) {
-                        if (isset($res[0][$limit][0])) {
-                            $cachedlimits[$context][$extension][$limit] =
-                                $res[0][$limit][0];
-                        } else {
-                            # If no value is provided use the context limit
-                            $cachedlimits[$context][$extension][$limit] =
-                                $cachedlimits[$context][$limit];
-                        }
-                    }
-                } else {
-                    foreach ($limits as $limit) {
-                        $cachedlimits[$context][$extension][$limit] =
-                            $cachedlimits[$context][$limit];
-                    }
-                }
-                return $cachedlimits[$context][$extension];
-            }
-            return $cachedlimits[$context];
-        }
-    }
-
-    /**
-     * Save a user to the LDAP tree
-     *
-     * @param string $context Context to which the user should be added
-     *
-     * @param string $extension Extension to be saved
-     *
-     * @param array $userdetails Phone numbers, PIN, options, etc to be saved
-     *
-     * @return TRUE on success, PEAR::Error object on error
-     */
-    function saveUser($context, $extension, $userdetails)
-    {
-        $ldapKey = &$this->_ldapKey;
-        $appKey = &$this->_appKey;
-        # FIXME Access Control/Authorization
-        if (
-            !(Shout::checkRights("shout:contexts:$context:users", PERMS_EDIT, 1))
-            &&
-            !($userdetails[$appKey] == Auth::getAuth())
-            ) {
-            return PEAR::raiseError("No permission to modify users in this " .
-                "context.");
-        }
-
-        $contexts = &$this->getContexts();
-//         $domain = $contexts[$context]['domain'];
-
-        # Check to ensure the extension is unique within this context
-        $filter = "(&(objectClass=asteriskVoiceMailbox)(context=$context))";
-        $reqattrs = array('dn', $ldapKey);
-        $res = @ldap_search($this->_LDAP,
-            SHOUT_USERS_BRANCH . ',' . $this->_params['basedn'],
-            $filter, $reqattrs);
-        if (!$res) {
-            return PEAR::raiseError('Unable to check directory for duplicate extension: ' .
-                ldap_error($this->_LDAP));
-        }
-        if (($res['count'] > 1) ||
-            ($res['count'] != 0 &&
-            !in_array($res[0][$ldapKey], $userdetails[$appKey]))) {
-            return PEAR::raiseError('Duplicate extension found.  Not saving changes.');
-        }
-
-        $entry = array(
-            'cn' => $userdetails['name'],
-            'sn' => $userdetails['name'],
-            'mail' => $userdetails['email'],
-            'uid' => $userdetails['email'],
-            'voiceMailbox' => $userdetails['newextension'],
-            'voiceMailboxPin' => $userdetails['mailboxpin'],
-            'context' => $context,
-            'asteriskUserDialOptions' => $userdetails['dialopts'],
-        );
-
-        if (!empty ($userdetails['telephonenumber'])) {
-            $entry['telephoneNumber'] = $userdetails['telephonenumber'];
-        }
-
-        $validusers = &$this->getUsers($context);
-        if (!isset($validusers[$extension])) {
-            # Test to see if we're modifying an existing user that has
-            # no telephone system objectClasses and update that object/user
-            $rdn = $ldapKey.'='.$userdetails[$appKey].',';
-            $branch = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
-
-            # This test is something of a hack.  I want a cheap way to check
-            # for the existance of an object.  I don't want to do a full search
-            # so instead I compare that the dn equals the dn.  If the object
-            # exists then it'll return true.  If the object doesn't exist,
-            # it'll return error.  If it ever returns false something wierd
-            # is going on.
-            $res = @ldap_compare($this->_LDAP, $rdn.$branch,
-                    $ldapKey, $userdetails[$appKey]);
-            if ($res === false) {
-                # We should never get here: a DN should ALWAYS match itself
-                return PEAR::raiseError("Internal Error: " . __FILE__ . " at " .
-                    __LINE__);
-            } elseif ($res === true) {
-                # The object/user exists but doesn't have the Asterisk
-                # objectClasses
-                $extension = $userdetails['newextension'];
-
-                # $tmp is the minimal information required to establish
-                # an account in LDAP as required by the objectClasses.
-                # The entry will be fully populated below.
-                $tmp = array();
-                $tmp['objectClass'] = array(
-                    'asteriskUser',
-                    'asteriskVoiceMailbox'
-                );
-                $tmp['voiceMailbox'] = $extension;
-                $tmp['context'] = $context;
-                $res = @ldap_mod_replace($this->_LDAP, $rdn.$branch, $tmp);
-                if (!$res) {
-                    return PEAR::raiseError("Unable to modify the user: " .
-                        ldap_error($this->_LDAP));
-                }
-
-                # Populate the $validusers array to make the edit go smoothly
-                # below
-                $validusers[$extension] = array();
-                $validusers[$extension][$appKey] = $userdetails[$appKey];
-
-                # The remainder of the work is done at the outside of the
-                # parent if() like a normal edit.
-
-            } elseif ($res === -1) {
-                # We must be adding a new user.
-                $entry['objectClass'] = array(
-                    'top',
-                    'person',
-                    'organizationalPerson',
-                    'inetOrgPerson',
-                    'hordePerson',
-                    'asteriskUser',
-                    'asteriskVoiceMailbox'
-                );
-
-                # Check to see if the maximum number of users for this context
-                # has been reached
-                $limits = $this->getLimits($context);
-                if (is_a($limits, "PEAR_Error")) {
-                    return $limits;
-                }
-                if (count($validusers) >= $limits['asteriskusersmax']) {
-                    return PEAR::raiseError('Maximum number of users reached.');
-                }
-
-                $res = @ldap_add($this->_LDAP, $rdn.$branch, $entry);
-                if (!$res) {
-                    return PEAR::raiseError('LDAP Add failed: ' .
-                        ldap_error($this->_LDAP));
-                }
-
-                return true;
-            } elseif (is_a($res, 'PEAR_Error')) {
-                # Some kind of internal error; not even sure if this is a
-                # possible outcome or not but I'll play it safe.
-                return $res;
-            }
-        }
-
-        # Anything after this point is an edit.
-
-        # Check to see if the object needs to be renamed (DN changed)
-        if ($validusers[$extension][$appKey] != $entry[$ldapKey]) {
-            $oldrdn = $ldapKey.'='.$validusers[$extension][$appKey];
-            $oldparent = SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
-            $newrdn = $ldapKey.'='.$entry[$ldapKey];
-            $res = @ldap_rename($this->_LDAP, "$oldrdn,$oldparent",
-                $newrdn, $oldparent, true);
-            if (!$res) {
-                return PEAR::raiseError('LDAP Rename failed: ' .
-                    ldap_error($this->_LDAP));
-            }
-        }
-
-        # Update the object/user
-        $dn = $ldapKey.'='.$entry[$ldapKey];
-        $dn .= ','.SHOUT_USERS_BRANCH.','.$this->_params['basedn'];
-        $res = @ldap_modify($this->_LDAP, $dn, $entry);
-        if (!$res) {
-            return PEAR::raiseError('LDAP Modify failed: ' .
-                ldap_error($this->_LDAP));
-        }
-
-        # We must have been successful
-        return true;
-    }
-
-    /**
-     * Deletes a user from the LDAP tree
-     *
-     * @param string $context Context to delete the user from
-     * @param string $extension Extension of the user to be deleted
-     *
-     * @return boolean True on success, PEAR::Error object on error
-     */
-    function deleteUser($context, $extension)
-    {
-        $ldapKey = &$this->_ldapKey;
-        $appKey = &$this->_appKey;
-
-        if (!Shout::checkRights("shout:contexts:$context:users",
-            PERMS_DELETE, 1)) {
-            return PEAR::raiseError("No permission to delete users in this " .
-                "context.");
-        }
-
-        $validusers = $this->getUsers($context);
-        if (!isset($validusers[$extension])) {
-            return PEAR::raiseError("That extension does not exist.");
-        }
-
-        $dn = "$ldapKey=".$validusers[$extension][$appKey];
-        $dn .= ',' . SHOUT_USERS_BRANCH . ',' . $this->_params['basedn'];
-
-        $res = @ldap_delete($this->_LDAP, $dn);
-        if (!$res) {
-            return PEAR::raiseError("Unable to delete $extension from " .
-                "$context: " . ldap_error($this->_LDAP));
-        }
-        return true;
-    }
-
-
-    /* Needed because uksort can't take a classed function as its callback arg */
-    function _sortexten($e1, $e2)
-    {
-        print "$e1 and $e2\n";
-        $ret =  Shout::extensort($e1, $e2);
-        print "returning $ret";
-        return $ret;
-    }
-
-    /**
-     * Attempts to open a connection to the LDAP server.
-     *
-     * @return boolean    True on success; exits (Horde::fatal()) on error.
-     *
-     * @access private
-     */
-    function _connect()
-    {
-        if (!$this->_connected) {
-            # FIXME What else is needed for this assert?
-            Horde::assertDriverConfig($this->_params, 'storage',
-                array('hostspec', 'basedn', 'binddn', 'password'));
-
-            # FIXME Add other sane defaults here (mostly objectClass related)
-            if (!isset($this->_params['userObjectclass'])) {
-                $this->_params['userObjectclass'] = 'asteriskUser';
-            }
-
-            $this->_LDAP = ldap_connect($this->_params['hostspec'], 389); #FIXME
-            if (!$this->_LDAP) {
-                Horde::fatal("Unable to connect to LDAP server $hostname on
-$port", __FILE__, __LINE__); #FIXME: $port
-            }
-            $res = ldap_set_option($this->_LDAP, LDAP_OPT_PROTOCOL_VERSION,
-$this->_params['version']);
-            if (!$res) {
-                return PEAR::raiseError("Unable to set LDAP protocol version");
-            }
-            $res = ldap_bind($this->_LDAP, $this->_params['binddn'],
-$this->_params['password']);
-            if (!$res) {
-                return PEAR::raiseError("Unable to bind to the LDAP server.
-Check authentication credentials.");
-            }
-
-            $this->_connected = true;
-        }
-        return true;
-    }
-}