Overview

Packages

  • None
  • Shout

Classes

  • AccountDetailsForm
  • ConferenceDetailsForm
  • DeviceDetailsForm
  • ExtensionDetailsForm
  • MenuForm
  • NumberDetailsForm
  • RecordingDetailsForm
  • Shout
  • Shout_Ajax_Application
  • Shout_Driver
  • Shout_Driver_Ldap
  • Shout_Driver_Sql
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Provides the LDAP backend driver for the Shout application.
  4:  *
  5:  * Copyright 2005-2010 Alkaloid Networks LLC (http://projects.alkaloid.net)
  6:  *
  7:  * See the enclosed file COPYING for license information (BSD). If you
  8:  * did not receive this file, see
  9:  * http://www.opensource.org/licenses/bsd-license.php.
 10:  *
 11:  * @author  Ben Klang <ben@alkaloid.net>
 12:  * @package Shout
 13:  */
 14: 
 15: class Shout_Driver_Ldap extends Shout_Driver
 16: {
 17:     var $_ldapKey;  // Index used for storing objects
 18:     var $_appKey;   // Index used for moving info to/from the app
 19: 
 20:     /**
 21:      * Handle for the current database connection.
 22:      * @var object LDAP $_LDAP
 23:      */
 24:     private $_LDAP;
 25: 
 26:     /**
 27:      * Boolean indicating whether or not we're connected to the LDAP
 28:      * server.
 29:      * @var boolean $_connected
 30:      */
 31:     private $_connected = false;
 32: 
 33: 
 34:     /**
 35:     * Constructs a new Shout LDAP driver object.
 36:     *
 37:     * @param array  $params    A hash containing connection parameters.
 38:     */
 39:     function __construct($params = array())
 40:     {
 41:         parent::__construct($params);
 42:         $this->_connect();
 43:     }
 44: 
 45:     /**
 46:      * Get a list of users valid for the accounts
 47:      *
 48:      * @param string $account  Account in which to search
 49:      *
 50:      * @return array User information indexed by voice mailbox number
 51:      */
 52:     public function getExtensions($account)
 53:     {
 54:         if (empty($account)) {
 55:             throw new Shout_Exception('Must specify an account code');
 56:         }
 57:         static $entries = array();
 58:         if (isset($entries[$account])) {
 59:             return $entries[$account];
 60:         }
 61: 
 62:         $this->_params['basedn'];
 63: 
 64:         $filter  = '(&';
 65:         $filter .= '(objectClass=AsteriskVoiceMail)';
 66:         $filter .= '(objectClass=AsteriskUser)';
 67:         $filter .= '(AstContext='.$account.')';
 68:         $filter .= ')';
 69: 
 70:         $attributes = array(
 71:             'cn',
 72:             'AstVoicemailEmail',
 73:             'AstVoicemailMailbox',
 74:             'AstVoicemailPassword',
 75:             'AstVoicemailOptions',
 76:             'AstVoicemailPager',
 77:             'telephoneNumber',
 78:             'AstUserChannel'
 79:         );
 80: 
 81:         $search = ldap_search($this->_LDAP, $this->_params['basedn'], $filter, $attributes);
 82:         if ($search === false) {
 83:             throw new Shout_Exception("Unable to search directory: " .
 84:                 ldap_error($this->_LDAP), ldap_errno($this->_LDAP));
 85:         }
 86: 
 87:         $res = ldap_get_entries($this->_LDAP, $search);
 88:         if ($res === false) {
 89:             throw new Shout_Exception("Unable to fetch results from directory: " .
 90:                 ldap_error($this->_LDAP), ldap_errno($this->_LDAP));
 91:         }
 92: 
 93:         // ATTRIBUTES RETURNED FROM ldap_get_entries ARE ALL LOWER CASE!!
 94:         // It's a PHP thing.
 95:         $entries[$account] = array();
 96:         $i = 0;
 97:         while ($i < $res['count']) {
 98:             list($extension) = explode('@', $res[$i]['astvoicemailmailbox'][0]);
 99:             $entries[$account][$extension] = array('extension' => $extension);
100: 
101:             $j = 0;
102:             $entries[$account][$extension]['mailboxopts'] = array();
103:             if (empty($res[$i]['astvoicemailoptions']['count'])) {
104:                 $res[$i]['astvoicemailoptions']['count'] = -1;
105:             }
106:             while ($j < $res[$i]['astvoicemailoptions']['count']) {
107:                 $entries[$account][$extension]['mailboxopts'][] =
108:                     $res[$i]['astvoicemailoptions'][$j];
109:                 $j++;
110:             }
111: 
112:             $entries[$account][$extension]['mailboxpin'] =
113:                 $res[$i]['astvoicemailpassword'][0];
114: 
115:             $entries[$account][$extension]['name'] =
116:                 $res[$i]['cn'][0];
117: 
118:             $entries[$account][$extension]['email'] =
119:                 $res[$i]['astvoicemailemail'][0];
120: 
121:             $entries[$account][$extension]['pageremail'] =
122:                 $res[$i]['astvoicemailpager'][0];
123: 
124:             $j = 0;
125:             $entries[$account][$extension]['numbers'] = array();
126:             if (empty($res[$i]['telephonenumber']['count'])) {
127:                 $res[$i]['telephonenumber']['count'] = -1;
128:             }
129:             while ($j < $res[$i]['telephonenumber']['count']) {
130:                 $entries[$account][$extension]['numbers'][] =
131:                     $res[$i]['telephonenumber'][$j];
132:                 $j++;
133:             }
134: 
135:             $j = 0;
136:             $entries[$account][$extension]['devices'] = array();
137:             if (empty($res[$i]['astuserchannel']['count'])) {
138:                 $res[$i]['astuserchannel']['count'] = -1;
139:             }
140:             while ($j < $res[$i]['astuserchannel']['count']) {
141:                 // Trim off the Asterisk channel type from the device string
142:                 $device = explode('/', $res[$i]['astuserchannel'][$j], 2);
143:                 $entries[$account][$extension]['devices'][] = $device[1];
144:                 $j++;
145:             }
146: 
147: 
148:             $i++;
149: 
150:         }
151: 
152:         ksort($entries[$account]);
153: 
154:         return($entries[$account]);
155:     }
156: 
157:     /**
158:      * Add a new destination valid for this extension.
159:      * A destination is either a telephone number or a VoIP device.
160:      *
161:      * @param string $account      Account for the extension
162:      * @param string $extension    Extension for which to return destinations
163:      * @param string $type         Destination type ("device" or "number")
164:      * @param string $destination  The destination itself
165:      *
166:      * @return boolean  True on success.
167:      */
168:     function addDestination($account, $extension, $type, $destination)
169:     {
170:         // FIXME: Permissions check
171:         $dn = $this->_getExtensionDn($account, $extension);
172:         $attr = $this->_getDestAttr($type, $destination);
173: 
174:         $res = ldap_mod_add($this->_LDAP, $dn, $attr);
175:         if ($res === false) {
176:             $msg = sprintf('Error while modifying the LDAP entry.  Code %s; Message "%s"',
177:                            ldap_errno($this->_LDAP), ldap_error($this->_LDAP));
178:             Horde::logMessage($msg, 'ERR');
179:             throw new Shout_Exception(_("Internal error modifing the directory.  Details have been logged for the administrator."));
180:         }
181: 
182:         return true;
183:     }
184: 
185:     /**
186:      * Get a list of destinations valid for this extension.
187:      * A destination is either a telephone number or a VoIP device.
188:      *
189:      * @param string $account    Account for the extension
190:      * @param string $extension  Extension for which to return destinations
191:      */
192:     function getDestinations($account, $extension)
193:     {
194:         // FIXME: LDAP filter injection
195:         $filter = '(&(AstContext=%s)(AstVoicemailMailbox=%s))';
196:         $filter = sprintf($filter, $account, $extension);
197: 
198:         $attrs = array('telephoneNumber', 'AstUserChannel');
199: 
200:         $res = ldap_search($this->_LDAP, $this->_params['basedn'],
201:                            $filter, $attrs);
202: 
203:         if ($res === false) {
204:             $msg = sprintf('Error while searching LDAP.  Code %s; Message "%s"',
205:                            ldap_errno($this->_LDAP), ldap_error($this->_LDAP));
206:             Horde::logMessage($msg, 'ERR');
207:             throw new Shout_Exception(_("Internal error searching the directory."));
208:         }
209: 
210:         $res = ldap_get_entries($this->_LDAP, $res);
211: 
212:         if ($res === false) {
213:             $msg = sprintf('Error while searching LDAP.  Code %s; Message "%s"',
214:                            ldap_errno($this->_LDAP), ldap_error($this->_LDAP));
215:             Horde::logMessage($msg, 'ERR');
216:             throw new Shout_Exception(_("Internal error searching the directory."));
217:         }
218: 
219:         if ($res['count'] != 1) {
220:             $msg = sprintf('Error while searching LDAP.  Code %s; Message "%s"',
221:                            ldap_errno($this->_LDAP), ldap_error($this->_LDAP));
222:             Horde::logMessage($msg, 'ERR');
223:             throw new Shout_Exception(_("Wrong number of entries found for this search."));
224:         }
225: 
226:         return array('numbers' => $res['telephonenumbers'],
227:                      'devices' => $res['astuserchannel']);
228:     }
229: 
230:     function deleteDestination($account, $extension, $type, $destination)
231:     {
232:         $dn = $this->_getExtensionDn($account, $extension);
233:         $attr = $this->_getDestAttr($type, $destination);
234: 
235:         $res = ldap_mod_del($this->_LDAP, $dn, $attr);
236:         if ($res === false) {
237:             $msg = sprintf('Error while modifying the LDAP entry.  Code %s; Message "%s"',
238:                            ldap_errno($this->_LDAP), ldap_error($this->_LDAP));
239:             Horde::logMessage($msg, 'ERR');
240:             throw new Shout_Exception(_("Internal error modifing the directory.  Details have been logged for the administrator."));
241:         }
242: 
243:         return true;
244:     }
245: 
246:     protected function _getDestAttr($type, $destination)
247:     {
248:         switch ($type) {
249:         case 'number':
250:             // FIXME: Strip this number down to just digits
251:             // FIXME: Add check for non-international numbers?
252:             $attr = array('telephoneNumber' => $destination);
253:             break;
254: 
255:         case 'device':
256:             // FIXME: Check that the device is valid and associated with this
257:             // account.
258:             // FIXME: Allow for different device types
259:             $attr = array('AstUserChannel' => "SIP/" . $destination);
260:             break;
261: 
262:         default:
263:             throw new Shout_Exception(_("Invalid destination type specified."));
264:             break;
265:         }
266: 
267:         return $attr;
268:     }
269: 
270:     /**
271:      * Save an extension to the LDAP tree
272:      *
273:      * @param string $account Account to which the user should be added
274:      *
275:      * @param string $extension Extension to be saved
276:      *
277:      * @param array $details Phone numbers, PIN, options, etc to be saved
278:      *
279:      * @return TRUE on success, PEAR::Error object on error
280:      * @throws Shout_Exception
281:      */
282:     public function saveExtension($account, $extension, $details)
283:     {
284:         // Check permissions
285:         parent::saveExtension($account, $extension, $details);
286: 
287:         // FIXME: Fix and uncomment the below
288: //        // Check to ensure the extension is unique within this account
289: //        $filter = "(&(objectClass=AstVoicemailMailbox)(context=$account))";
290: //        $reqattrs = array('dn', $ldapKey);
291: //        $res = @ldap_search($this->_LDAP, $this->_params['basedn'],
292: //                            $filter, $reqattrs);
293: //        if ($res === false) {
294: //            $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
295: //                                                  ldap_error($this->_LDAP));
296: //            Horde::logMessage($msg, 'ERR');
297: //            throw new Shout_Exception(_("Error while searching the directory.  Details have been logged for the administrator."));
298: //        }
299: //        if (($res['count'] != 1) ||
300: //            ($res['count'] != 0 &&
301: //            !in_array($res[0][$ldapKey], $details[$appKey]))) {
302: //            throw new Shout_Exception(_("Duplicate extension found.  Not saving changes."));
303: //        }
304:         // FIXME: Quote these strings
305:         $uid = $extension . '@' . $account;
306:         $entry = array(
307:             'objectClass' => array('top', 'account',
308:                                    'AsteriskVoicemail', 'AsteriskUser'),
309:             'uid' => $uid,
310:             'cn' => $details['name'],
311:             'AstVoicemailEmail' => $details['email'],
312:             'AstVoicemailMailbox' => $extension,
313:             'AstVoicemailPassword' => $details['mailboxpin'],
314:             'AstContext' => $account,
315:         );
316:         $rdn = 'uid=' . $uid;
317:         $dn = $rdn . ',' . $this->_params['basedn'];
318: 
319:         if (!empty($details['oldextension'])) {
320:             // This is a change to an existing extension
321:             // First, identify the DN to modify
322:             // FIXME: Quote these strings
323:             $olddn = $this->_getExtensionDn($account, $extension);
324: 
325:             // If the extension has changed we need to perform an object rename
326:             if ($extension != $details['oldextension']) {
327:                 $res = ldap_rename($this->_LDAP, $olddn, $rdn,
328:                                    $this->_params['basedn'], true);
329: 
330:                 if ($res === false) {
331:                     $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
332:                                                       ldap_error($this->_LDAP));
333:                     Horde::logMessage($msg, 'ERR');
334:                     throw new Shout_Exception(_("Error while modifying the directory.  Details have been logged for the administrator."));
335:                 }
336:             }
337: 
338:             // Now apply the changes
339:             // Avoid changing the objectClass, just in case
340:             unset($entry['objectClass']);
341:             $res = ldap_modify($this->_LDAP, $dn, $entry);
342:             if ($res === false) {
343:                 $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
344:                                                       ldap_error($this->_LDAP));
345:                 Horde::logMessage($msg, 'ERR');
346:                 throw new Shout_Exception(_("Error while modifying the directory.  Details have been logged for the administrator."));
347:             }
348: 
349:             return true;
350:         } else {
351:             // This is an add of a new extension
352:             $res = ldap_add($this->_LDAP, $dn, $entry);
353:             if ($res === false) {
354:                 $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
355:                                                       ldap_error($this->_LDAP));
356:                 Horde::logMessage($msg, 'ERR');
357:                 throw new Shout_Exception(_("Error while modifying the directory.  Details have been logged for the administrator."));
358:             }
359:             return true;
360:         }
361: 
362:         // Catch-all.  We should not get here.
363:         throw new Shout_Exception(_("Unspecified error."));
364:     }
365: 
366:     /**
367:      * Deletes an extension from the LDAP tree
368:      *
369:      * @param string $account Account to delete the user from
370:      * @param string $extension Extension of the user to be deleted
371:      *
372:      * @return boolean True on success, PEAR::Error object on error
373:      */
374:     public function deleteExtension($account, $extension)
375:     {
376:         // Check permissions
377:         parent::deleteExtension($account, $extension);
378: 
379:         $dn = $this->_getExtensionDn($account, $extension);
380: 
381:         $res = @ldap_delete($this->_LDAP, $dn);
382:         if ($res === false) {
383:             $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
384:                                                   ldap_error($this->_LDAP));
385:             Horde::logMessage($msg, 'ERR');
386:             throw new Horde_Exception(_("Error while deleting from the directory.  Details have been logged for the administrator."));
387:         }
388: 
389:         return true;
390:     }
391: 
392:     /**
393:      *
394:      * @param <type> $account
395:      * @param <type> $extension
396:      */
397:     protected function _getExtensionDn($account, $extension)
398:     {
399:         // FIXME: Sanitize filter string against LDAP injection
400:         $filter = '(&(AstVoicemailMailbox=%s)(AstContext=%s))';
401:         $filter = sprintf($filter, $extension, $account);
402:         $attributes = array('dn');
403: 
404:         $res = ldap_search($this->_LDAP, $this->_params['basedn'],
405:                            $filter, $attributes);
406:         if ($res === false) {
407:             $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
408:                                                   ldap_error($this->_LDAP));
409:             Horde::logMessage($msg, 'ERR');
410:             throw new Shout_Exception(_("Error while searching the directory.  Details have been logged for the administrator."));
411:         }
412: 
413:         if (ldap_count_entries($this->_LDAP, $res) < 1) {
414:             throw new Shout_Exception(_("No such extension found."));
415:         }
416: 
417:         $res = ldap_first_entry($this->_LDAP, $res);
418:         if ($res === false) {
419:             $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
420:                                                   ldap_error($this->_LDAP));
421:             Horde::logMessage($msg, 'ERR');
422:             throw new Shout_Exception(_("Error while searching the directory.  Details have been logged for the administrator."));
423:         }
424: 
425:         $dn = ldap_get_dn($this->_LDAP, $res);
426:         if ($dn === false) {
427:             $msg = sprintf('LDAP Error (%s): %s', ldap_errno($this->_LDAP),
428:                                                   ldap_error($this->_LDAP));
429:             Horde::logMessage($msg, 'ERR');
430:             throw new Shout_Exception(_("Internal LDAP error.  Details have been logged for the administrator."));
431:         }
432: 
433:         return $dn;
434:     }
435: 
436:     /**
437:      * Attempts to open a connection to the LDAP server.
438:      *
439:      * @return boolean    True on success.
440:      * @throws Shout_Exception
441:      *
442:      * @access private
443:      */
444:     protected function _connect()
445:     {
446:         if ($this->_connected) {
447:             return;
448:         }
449: 
450:         if (!Horde_Util::extensionExists('ldap')) {
451:             throw new Shout_Exception('Required LDAP extension not found.');
452:         }
453: 
454:         Horde::assertDriverConfig($this->_params, $this->_params['class'],
455:             array('hostspec', 'basedn', 'writedn'));
456: 
457:         /* Open an unbound connection to the LDAP server */
458:         $conn = ldap_connect($this->_params['hostspec'], $this->_params['port']);
459:         if (!$conn) {
460:              Horde::logMessage(
461:                 sprintf('Failed to open an LDAP connection to %s.',
462:                         $this->_params['hostspec']), 'ERR');
463:             throw new Shout_Exception('Internal LDAP error. Details have been logged for the administrator.');
464:         }
465: 
466:         /* Set hte LDAP protocol version. */
467:         if (isset($this->_params['version'])) {
468:             $result = ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION,
469:                                        $this->_params['version']);
470:             if ($result === false) {
471:                 Horde::logMessage(
472:                     sprintf('Set LDAP protocol version to %d failed: [%d] %s',
473:                             $this->_params['version'],
474:                             ldap_errno($conn),
475:                             ldap_error($conn)), 'WARN');
476:                 throw new Shout_Exception('Internal LDAP error. Details have been logged for the administrator.', ldap_errno($conn));
477:             }
478:         }
479: 
480:         /* Start TLS if we're using it. */
481:         if (!empty($this->_params['tls'])) {
482:             if (!@ldap_start_tls($conn)) {
483:                 Horde::logMessage(
484:                     sprintf('STARTTLS failed: [%d] %s',
485:                             @ldap_errno($this->_ds),
486:                             @ldap_error($this->_ds)), 'ERR');
487:             }
488:         }
489: 
490:         /* If necessary, bind to the LDAP server as the user with search
491:          * permissions. */
492:         if (!empty($this->_params['searchdn'])) {
493:             $bind = ldap_bind($conn, $this->_params['searchdn'],
494:                               $this->_params['searchpw']);
495:             if ($bind === false) {
496:                 Horde::logMessage(
497:                     sprintf('Bind to server %s:%d with DN %s failed: [%d] %s',
498:                             $this->_params['hostspec'],
499:                             $this->_params['port'],
500:                             $this->_params['searchdn'],
501:                             @ldap_errno($conn),
502:                             @ldap_error($conn)), 'ERR');
503:                 throw new Shout_Exception('Internal LDAP error. Details have been logged for the administrator.', ldap_errno($conn));
504:             }
505:         }
506: 
507:         /* Store the connection handle at the instance level. */
508:         $this->_LDAP = $conn;
509:     }
510: 
511: }
512: 
API documentation generated by ApiGen