Overview

Packages

  • Prefs

Classes

  • Horde_Prefs
  • Horde_Prefs_Cache_Base
  • Horde_Prefs_Cache_Null
  • Horde_Prefs_Cache_Session
  • Horde_Prefs_CategoryManager
  • Horde_Prefs_Exception
  • Horde_Prefs_Identity
  • Horde_Prefs_Scope
  • Horde_Prefs_Storage_Base
  • Horde_Prefs_Storage_File
  • Horde_Prefs_Storage_Imsp
  • Horde_Prefs_Storage_KolabImap
  • Horde_Prefs_Storage_Ldap
  • Horde_Prefs_Storage_Null
  • Horde_Prefs_Storage_Sql
  • Horde_Prefs_Translation
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Preferences storage implementation for LDAP servers.
  4:  *
  5:  * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
  6:  *
  7:  * See the enclosed file COPYING for license information (LGPL). If you
  8:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  9:  *
 10:  * @author   Jon Parise <jon@horde.org>
 11:  * @author   Ben Klang <ben@alkaloid.net>
 12:  * @author   Michael Slusarz <slusarz@horde.org>
 13:  * @category Horde
 14:  * @package  Prefs
 15:  */
 16: class Horde_Prefs_Storage_Ldap extends Horde_Prefs_Storage_Base
 17: {
 18:     /**
 19:      * Handle for the current LDAP connection.
 20:      *
 21:      * @var resource
 22:      */
 23:     protected $_connection;
 24: 
 25:     /**
 26:      * Boolean indicating whether or not we're connected to the LDAP server.
 27:      *
 28:      * @var boolean
 29:      */
 30:     protected $_connected = false;
 31: 
 32:     /**
 33:      * String holding the user's DN.
 34:      *
 35:      * @var string
 36:      */
 37:     protected $_dn = '';
 38: 
 39:     /**
 40:      * Constructor.
 41:      *
 42:      * @param string $user   The username.
 43:      * @param array $params  Configuration options:
 44:      * <pre>
 45:      * basedn - (string) [REQUIRED] The base DN for the LDAP server.
 46:      * hostspec - (string) [REQUIRED] The hostname of the LDAP server.
 47:      * uid - (string) [REQUIRED] The username search key.
 48:      * writeas - (string) [REQUIRED] One of "user", "admin", or "search"
 49:      *
 50:      * Optional parameters:
 51:      * binddn - (string) The DN of the administrative account to bind for
 52:      *          write operations.
 53:      * bindpw - (string) binddn's password for bind authentication.
 54:      * port - (integer) The port of the LDAP server.
 55:      *        DEFAULT: 389
 56:      * searchdn - (string) The DN of a user with search permissions on the
 57:      *            directory.
 58:      * searchpw - (string) searchdn's password for binding.
 59:      * tls - (boolean) Whether to use TLS connections.
 60:      *       DEFAULT: false
 61:      * version - (integer) The version of the LDAP protocol to use.
 62:      *           DEFAULT: NONE (system default will be used)
 63:      * </pre>
 64:      */
 65:     public function __construct($user, array $params = array())
 66:     {
 67:         throw new Horde_Prefs_Exception('This driver needs to be refactored to use Horde_Ldap.');
 68:         /* If a valid server port has not been specified, set the default. */
 69:         if (!isset($params['port']) || !is_integer($params['port'])) {
 70:             $params['port'] = 389;
 71:         }
 72: 
 73:         parent::__construct($user, $params);
 74:     }
 75: 
 76:     /**
 77:      */
 78:     public function get($scope_ob)
 79:     {
 80:         $this->_connect();
 81: 
 82:         // Search for the multi-valued field containing the array of
 83:         // preferences.
 84:         $search = @ldap_search(
 85:             $this->_connection,
 86:             $this->_params['basedn'],
 87:             $this->_params['uid'] . '=' . $this->params['user'],
 88:             array($scope_ob->scope . 'Prefs'));
 89:         if ($search === false) {
 90:             throw new Horde_Prefs_Exception(sprintf('Error while searching for the user\'s prefs: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
 91:         }
 92: 
 93:         $result = @ldap_get_entries($this->_connection, $search);
 94:         if ($result === false) {
 95:             throw new Horde_Prefs_Exception(sprintf('Error while retrieving LDAP search results for the user\'s prefs: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
 96:         }
 97: 
 98:         // Preferences are stored as colon-separated name:value pairs.
 99:         // Each pair is stored as its own attribute off of the multi-
100:         // value attribute named in: $scope_ob->scope . 'Prefs'
101: 
102:         // ldap_get_entries() converts attribute indexes to lowercase.
103:         $field = Horde_String::lower($scope_ob->scope . 'prefs');
104:         $prefs = isset($result[0][$field])
105:             ? $result[0][$field]
106:             : array();
107: 
108:         foreach ($prefs as $prefstr) {
109:             // If the string doesn't contain a colon delimiter, skip it.
110:             if (strpos($prefstr, ':') !== false) {
111:                 // Split the string into its name:value components.
112:                 list($name, $val) = explode(':', $prefstr, 2);
113:                 $scope_ob->set($name, base64_decode($val));
114:             }
115:         }
116: 
117:         return $scope_ob;
118:     }
119: 
120:     /**
121:      */
122:     public function store($scope_ob)
123:     {
124:         $this->_connect();
125: 
126:         // Build a hash of the preferences and their values that need
127:         // to be stored on the LDAP server. Because we have to update
128:         // all of the values of a multi-value entry wholesale, we
129:         // can't just pick out the dirty preferences; we must update
130:         // the entire dirty scope.
131:         $new_vals = array();
132: 
133:         /* Driver has no support for storing locked status. */
134:         foreach ($scope_ob->getDirty() as $name) {
135:             $new_vals[$scope_ob->scope . 'Prefs'][] = $name . ':' . base64_encode($scope_ob->get($name));
136:         }
137: 
138:         // Entries must have the objectclasses 'top' and 'hordeperson'
139:         // to successfully store LDAP prefs. Check for both of them,
140:         // and add them if necessary.
141:         $search = @ldap_search(
142:             $this->_connection,
143:             $this->_params['basedn'],
144:             $this->_params['uid'] . '=' . $this->prefs['user'],
145:             array('objectclass')
146:         );
147:         if ($search === false) {
148:             throw new Horde_Prefs_Exception(sprintf('Error searching the directory for required objectClasses: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
149:         }
150: 
151:         $result = @ldap_get_entries($this->_connection, $search);
152:         if ($result === false) {
153:             throw new Horde_Prefs_Exception(sprintf('Error retrieving results while checking for required objectClasses: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
154:         }
155: 
156:         if ($result['count'] > 0) {
157:             $hordeperson = $top = false;
158: 
159:             for ($i = 0; $i < $result[0]['objectclass']['count']; ++$i) {
160:                 if ($result[0]['objectclass'][$i] == 'top') {
161:                     $top = true;
162:                 } elseif ($result[0]['objectclass'][$i] == 'hordePerson') {
163:                     $hordeperson = true;
164:                 }
165:             }
166: 
167:             // Add any missing objectclasses.
168:             if (!$top) {
169:                 @ldap_mod_add($this->_connection, $this->_dn, array('objectclass' => 'top'));
170:             }
171: 
172:             if (!$hordeperson) {
173:                 @ldap_mod_add($this->_connection, $this->_dn, array('objectclass' => 'hordePerson'));
174:             }
175:         }
176: 
177:         // Send the hash to the LDAP server.
178:         $result = @ldap_mod_replace($this->_connection, $this->_dn, $new_vals);
179:         if ($result === false) {
180:             throw new Horde_Prefs_Exception(sprintf('Unable to modify user\'s objectClass for preferences: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
181:         }
182:     }
183: 
184:     /**
185:      */
186:     public function remove($scope = null, $pref = null)
187:     {
188:         // TODO: Implement scope/pref-level removal
189:         if (!is_null($scope) || !is_null($pref)) {
190:             throw new Horde_Prefs_Exception('Removal not supported.');
191:         }
192: 
193:         $this->_connect();
194: 
195:         // TODO: Move to horde/Core
196:         $attrs = $GLOBALS['registry']->listApps(array('inactive', 'active', 'hidden', 'notoolbar', 'admin'));
197:         foreach ($attrs as $key => $val) {
198:             $attrs[$key] = $val . 'Prefs';
199:         }
200: 
201:         $search = @ldap_read($this->_connection, $this->_dn,
202:                             'objectClass=hordePerson', $attrs, 1);
203:         if ($search === false) {
204:             throw new Horde_Prefs_Exception(sprintf('Error while getting preferenes from LDAP: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
205:         }
206: 
207:         $result = @ldap_get_entries($this->_connection, $search);
208:         if ($result === false) {
209:             throw new Horde_Prefs_Exception(sprintf('Error while retrieving results from LDAP: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
210:         }
211: 
212:         $attrs = array();
213:         for ($i = 0; $i < $result[0]['count']; $i++) {
214:             $attrs[$result[0][$i]] = array();
215:         }
216:         $result = @ldap_mod_del($this->_connection, $this->_dn, $attrs);
217:         if ($result === false) {
218:             throw new Horde_Prefs_Exception(sprintf('Unable to clear user\'s preferences: [%d] %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
219:         }
220:     }
221: 
222:     /* LDAP Helper functions.
223:      * TODO: Use Horde_LDAP */
224: 
225:     /**
226:      * Opens a connection to the LDAP server.
227:      *
228:      * @throws Horde_Prefs_Exception
229:      */
230:     protected function _connect()
231:     {
232:         if ($this->_connected) {
233:             return;
234:         }
235: 
236:         if (!Horde_Util::extensionExists('ldap')) {
237:             throw new Horde_Prefs_Exception('Required LDAP extension not found.');
238:         }
239: 
240:         Horde::assertDriverConfig($this->_params, 'prefs',
241:             array('hostspec', 'basedn', 'uid', 'writeas'),
242:             'preferences LDAP');
243: 
244:         /* Connect to the LDAP server anonymously. */
245:         $conn = ldap_connect($this->_params['hostspec'], $this->_params['port']);
246:         if (!$conn) {
247:             throw new Horde_Prefs_Exception(sprintf('Failed to open an LDAP connection to %s.', $this->_params['hostspec']));
248:         }
249: 
250:         /* Set the LDAP protocol version. */
251:         if (isset($this->_params['version'])) {
252:             $result = @ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION,
253:                                        $this->_params['version']);
254:             if ($result === false) {
255:                 throw new Horde_Prefs_Exception(sprintf('Set LDAP protocol version to %d failed: [%d] %s', $this->_params['version'], @ldap_errno($conn), @ldap_error($conn)));
256:             }
257:         }
258: 
259:         /* Start TLS if we're using it. */
260:         if (!empty($this->_params['tls'])) {
261:             @ldap_start_tls($conn);
262:         }
263: 
264:         /* If necessary, bind to the LDAP server as the user with search
265:          * permissions. */
266:         if (!empty($this->_params['searchdn'])) {
267:             $bind = @ldap_bind($conn, $this->_params['searchdn'],
268:                                $this->_params['searchpw']);
269:             if ($bind === false) {
270:                 throw new Horde_Prefs_Exception(sprintf('Bind to server %s:%d with DN %s failed: [%d] %s', $this->_params['hostspec'], $this->_params['port'], $this->_params['searchdn'], @ldap_errno($conn), @ldap_error($conn)));
271:             }
272:         }
273: 
274:         /* Register our callback function to handle referrals. */
275:         if (function_exists('ldap_set_rebind_proc')) {
276:             $result = @ldap_set_rebind_proc($conn, array($this, 'rebindProc'));
277:             if ($result === false) {
278:                 throw new Horde_Prefs_Exception(sprintf('Setting referral callback failed: [%d] %s', @ldap_errno($conn), @ldap_error($conn)));
279:             }
280:         }
281: 
282:         /* Store the connection handle at the instance level. */
283:         $this->_connection = $conn;
284: 
285:         /* Search for the user's full DN. */
286:         $search = @ldap_search($this->_connection, $this->_params['basedn'],
287:                                $this->_params['uid'] . '=' . $this->params['user'], array('dn'));
288:         if ($search === false) {
289:             throw new Horde_Prefs_Exception(sprintf('Error while searching the directory for the user\'s DN: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
290:         }
291: 
292:         $result = @ldap_get_entries($this->_connection, $search);
293:         if ($result === false) {
294:             throw new Horde_Prefs_Exception(sprintf('Error while retrieving LDAP search results for the user\'s DN: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
295:         }
296: 
297:         if ($result['count'] != 1) {
298:             throw new Horde_Prefs_Exception('Zero or more than one DN returned from search; unable to determine user\'s correct DN.');
299:         }
300:         $this->_dn = $result[0]['dn'];
301: 
302:         // Now we should have the user's DN.  Re-bind as appropriate with write
303:         // permissions to be able to store preferences.
304:         switch($this->_params['writeas']) {
305:         case 'user':
306:             $result = @ldap_bind($this->_connection,
307:                                  $this->_dn, $this->_opts['password']);
308:             break;
309: 
310:         case 'admin':
311:             $result = @ldap_bind($this->_connection,
312:                                  $this->_params['binddn'],
313:                                  $this->_params['bindpw']);
314:             break;
315: 
316:         case 'search':
317:             // Since we've already bound as the search DN above, no rebinding
318:             // is necessary.
319:             $result = true;
320:             break;
321:         }
322: 
323:         if ($result === false) {
324:             throw new Horde_Prefs_Exception(sprintf('Error rebinding for prefs writing: [%d]: %s', @ldap_errno($this->_connection), @ldap_error($this->_connection)));
325:         }
326: 
327:         // We now have a ready-to-use connection.
328:         $this->_connected = true;
329:     }
330: 
331:     /**
332:      * Callback function for LDAP referrals.  This function is called when an
333:      * LDAP operation returns a referral to an alternate server.
334:      *
335:      * @return integer  1 on error, 0 on success.
336:      */
337:     public function rebindProc($conn, $who)
338:     {
339:         /* Strip out the hostname we're being redirected to. */
340:         $who = preg_replace(array('|^.*://|', '|:\d*$|'), '', $who);
341: 
342:         /* Make sure the server we're being redirected to is in our list of
343:            valid servers. */
344:         if (strpos($this->_params['hostspec'], $who) === false) {
345:             if ($this->_opts['logger']) {
346:                 $this->_opts['logger']->log(sprintf('Referral target %s for DN %s is not in the authorized server list.', $who, $bind_dn), 'ERR');
347:             }
348:             return 1;
349:         }
350: 
351:         /* Figure out the DN of the authenticating user. */
352:         switch($this->_params['writeas']) {
353:         case 'user':
354:             $bind_dn = $this->_dn;
355:             $bind_pw = $this->_opts['password'];
356:             break;
357: 
358:         case 'admin':
359:             $bind_dn = $this->_params['binddn'];
360:             $bind_pw = $this->_params['bindpw'];
361:             break;
362: 
363:         case 'search':
364:             $bind_dn = $this->_params['searchdn'];
365:             $bind_dn = $this->_params['searchpw'];
366:             break;
367:         }
368: 
369:         /* Bind to the new server. */
370:         $bind = @ldap_bind($conn, $bind_dn, $bind_pw);
371:         if (($bind === false) && $this->_opts['logger']) {
372:             $this->_opts['logger']->log(sprintf('Rebind to server %s:%d with DN %s failed: [%d] %s', $this->_params['hostspec'], $this->_params['port'], $bind_dn, @ldap_errno($this->_connection), @ldap_error($this->_connection)), 'ERR');
373:         }
374: 
375:         return 0;
376:     }
377: 
378: 
379: }
380: 
API documentation generated by ApiGen