class_ldap.inc 46.19 KiB
<?php
/*
  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
  Copyright (C) 2003-2010  Cajus Pollmeier
  Copyright (C) 2003 Alejandro Escanero Blanco <aescanero@chaosdimension.org>
  Copyright (C) 1998  Eric Kilfoil <eric@ipass.net>
  Copyright (C) 2011-2016  FusionDirectory
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
/*!
 * \file class_ldap.inc
 * Source code for Class LDAP
/*!
 * \brief This class contains all ldap function needed to make
 * ldap operations easy
class LDAP
  var $hascon         = FALSE;
  var $reconnect      = FALSE;
  var $tls            = FALSE;
  /**
   * Connection identifier
   * @var resource|object|false
  var $cid            = FALSE;
  var $hasres         = [];
  var $sr             = [];
  var $re             = [];
  var $basedn         = "";
  /* 0 if we are fetching the first entry, otherwise 1 */
  var $start          = [];
  /* Any error messages to be returned can be put here */
  var $error          = "";
  var $srp            = 0;
  /* Information read from slapd.oc.conf */
  var $objectClasses    = [];
  /* the dn for the bind */
  var $binddn           = "";
  /* the dn's password for the bind */
  var $bindpw           = "";
  var $hostname         = "";
  var $follow_referral  = FALSE;
  var $referrals        = [];
  /* 0, empty or negative values will disable this check */
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
var $max_ldap_query_time = 0; /*! * \brief Create a LDAP connection * * \param string $binddn Bind of the DN * * \param string $bindpw Bind * * \param string $hostname The hostname * * \param boolean $follow_referral FALSE * * \param boolean $tls FALSE */ function __construct ($binddn, $bindpw, $hostname, $follow_referral = FALSE, $tls = FALSE) { global $config; $this->follow_referral = $follow_referral; $this->tls = $tls; $this->binddn = $binddn; $this->bindpw = $bindpw; $this->hostname = $hostname; /* Check if MAX_LDAP_QUERY_TIME is defined */ if (is_object($config) && ($config->get_cfg_value("ldapMaxQueryTime") != "")) { $str = $config->get_cfg_value("ldapMaxQueryTime"); $this->max_ldap_query_time = (float)($str); } $this->connect(); } /*! \brief Remove bogus resources after unserialize */ public function __wakeup () { $this->cid = FALSE; $this->hascon = FALSE; } /*! * \brief Initialize a LDAP connection * * Initializes a LDAP connection. * * \param string $server The server we are connecting to * * \param string $base The base of our ldap tree * * \param string $binddn Default: empty * * \param string $pass Default: empty * * \return LDAP object */ public static function init (string $server, string $base, string $binddn = '', string $pass = ''): LDAP { global $config; $ldap = new LDAP($binddn, $pass, $server, isset($config->current['LDAPFOLLOWREFERRALS']) && $config->current['LDAPFOLLOWREFERRALS'] == 'TRUE', isset($config->current['LDAPTLS']) && $config->current['LDAPTLS'] == 'TRUE'); /* Sadly we've no proper return values here. Use the error message instead. */ if (!$ldap->success()) { throw new FatalError(htmlescape(sprintf(_('FATAL: Error when connecting to LDAP. Server said "%s".'), $ldap->get_error()))); } /* Preset connection base to $base and return to caller */
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
$ldap->cd($base); return $ldap; } /*! * \brief Get the search ressource * * \return increase srp */ function getSearchResource () { $this->sr[$this->srp] = NULL; $this->start[$this->srp] = 0; $this->hasres[$this->srp] = FALSE; return $this->srp++; } /*! * \brief Function to fix problematic characters in DN's that are used for search requests. I.e. member=.... * * \param string $dn The DN */ static function prepare4filter ($dn) { trigger_error('deprecated, use ldap_escape_f instead'); return ldap_escape_f($dn); } /*! * \brief Error text that must be returned for invalid user or password * * This is useful to make sure the same error text is shown whether a user exists or not, when the password is not correct. */ static function invalidCredentialsError (): string { return _(ldap_err2str(49)); } /*! * \brief Create a connection to LDAP server * * The string $error containts result of the connection */ function connect () { $this->hascon = FALSE; $this->reconnect = FALSE; if ($this->cid = @ldap_connect($this->hostname)) { @ldap_set_option($this->cid, LDAP_OPT_PROTOCOL_VERSION, 3); if ($this->follow_referral) { @ldap_set_option($this->cid, LDAP_OPT_REFERRALS, 1); @ldap_set_rebind_proc($this->cid, [&$this, 'rebind']); } if ($this->tls) { @ldap_start_tls($this->cid); } $this->error = 'No Error'; $serverctrls = []; if (class_available('ppolicyAccount')) { $serverctrls = [['oid' => LDAP_CONTROL_PASSWORDPOLICYREQUEST]]; } $result = @ldap_bind_ext($this->cid, $this->binddn, $this->bindpw, $serverctrls); if (@ldap_parse_result($this->cid, $result, $errcode, $matcheddn, $errmsg, $referrals, $ctrls)) { if (isset($ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error'])) { $this->hascon = FALSE; switch ($ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error']) { case 0: /* passwordExpired - password has expired and must be reset */ $this->error = _('It seems your user password has expired. Please use <a href="recovery.php">password recovery</a> to change it.');
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
break; case 1: /* accountLocked */ $this->error = _('Account locked. Please contact your system administrator!'); break; case 2: /* changeAfterReset - password must be changed before the user will be allowed to perform any other operation */ $this->error = 'changeAfterReset'; break; case 3: /* passwordModNotAllowed */ case 4: /* mustSupplyOldPassword */ case 5: /* insufficientPasswordQuality */ case 6: /* passwordTooShort */ case 7: /* passwordTooYoung */ case 8: /* passwordInHistory */ default: $this->error = sprintf(_('Unexpected ppolicy error "%s", please contact the administrator'), $ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error']); break; } // Note: Also available: expire, grace } else { $this->hascon = ($errcode == 0); if ($errcode == 49) { $this->error = static::invalidCredentialsError(); } elseif (empty($errmsg)) { $this->error = ldap_err2str($errcode); } else { $this->error = $errmsg; } } } else { $this->error = 'Parsing of LDAP result from bind failed'; $this->hascon = FALSE; } } else { $this->error = 'Could not connect to LDAP server'; } logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'connect'); } /*! * \brief Rebind */ function rebind ($ldap, $referral) { $credentials = $this->get_credentials($referral); if (@ldap_bind($ldap, $credentials['ADMINDN'], $credentials['ADMINPASSWORD'])) { $this->error = "Success"; $this->hascon = TRUE; $this->reconnect = TRUE; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rebind'); return 0; } else { $this->error = "Could not bind to " . $credentials['ADMINDN']; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rebind'); return NULL; } } /*! * \brief Reconnect to LDAP server */ function reconnect ()
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
{ if ($this->reconnect) { $this->unbind(); } } /*! * \brief Unbind to LDAP server */ function unbind () { @ldap_unbind($this->cid); $this->cid = FALSE; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, '', 'unbind'); } /*! * \brief Disconnect to LDAP server */ function disconnect () { if ($this->hascon) { @ldap_close($this->cid); $this->hascon = FALSE; $this->cid = FALSE; } logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, '', 'disconnect'); } /*! * \brief Change directory * * \param string $dir The new directory */ function cd ($dir) { if ($dir == '..') { $this->basedn = $this->getParentDir(); } else { $this->basedn = $dir; } } /*! * \brief Accessor of the parent directory of the basedn * * \param string $basedn The basedn which we want the parent directory * * \return String, the parent directory */ function getParentDir ($basedn = '') { if ($basedn == '') { $basedn = $this->basedn; } return preg_replace("/[^,]*[,]*[ ]*(.*)/", "$1", $basedn); } /*! * \brief Search about filter * * \param integer $srp srp * * \param string $filter The filter * * \param array $attrs * * \param string $scope Scope of the search: subtree/base/one */ function search ($srp, $filter, $attrs = [], $scope = 'subtree', array $controls = NULL)
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
{ if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $startTime = microtime(TRUE); $this->clearResult($srp); switch (strtolower($scope)) { case 'base': if (isset($controls)) { $this->sr[$srp] = @ldap_read($this->cid, $this->basedn, $filter, $attrs, 0, 0, 0, LDAP_DEREF_NEVER, $controls); } else { $this->sr[$srp] = @ldap_read($this->cid, $this->basedn, $filter, $attrs); } break; case 'one': if (isset($controls)) { $this->sr[$srp] = @ldap_list($this->cid, $this->basedn, $filter, $attrs, 0, 0, 0, LDAP_DEREF_NEVER, $controls); } else { $this->sr[$srp] = @ldap_list($this->cid, $this->basedn, $filter, $attrs); } break; case 'subtree': default: if (isset($controls)) { $this->sr[$srp] = @ldap_search($this->cid, $this->basedn, $filter, $attrs, 0, 0, 0, LDAP_DEREF_NEVER, $controls); } else { $this->sr[$srp] = @ldap_search($this->cid, $this->basedn, $filter, $attrs); } break; } $this->error = @ldap_error($this->cid); $this->resetResult($srp); $this->hasres[$srp] = TRUE; /* Check if query took longer as specified in max_ldap_query_time */ $diff = microtime(TRUE) - $startTime; if ($this->max_ldap_query_time && ($diff > $this->max_ldap_query_time)) { $warning = new FusionDirectoryWarning(htmlescape(sprintf(_('LDAP performance is poor: last query took about %.2fs!'), $diff))); $warning->display(); } $this->log("LDAP operation: time=".$diff." operation=search('".$this->basedn."', '$filter')"); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'search(base="'.$this->basedn.'",scope="'.$scope.'",filter="'.$filter.'")'); return $this->sr[$srp]; } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'search(base="'.$this->basedn.'",scope="'.$scope.'",filter="'.$filter.'")'); return ""; } } /*! * \brief Parse last result * * \param integer $srp srp * */ function parse_result ($srp): array { if ($this->hascon && $this->hasres[$srp]) { if (ldap_parse_result($this->cid, $this->sr[$srp], $errcode, $matcheddn, $errmsg, $referrals, $controls)) { return [$errcode, $matcheddn, $errmsg, $referrals, $controls]; } throw new FusionDirectoryException(_('Parsing LDAP result failed')); } else { throw new FusionDirectoryException(_('No LDAP result to parse')); } }
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
/* * \brief List * * \param integer $srp * * \param string $filter Initialized at "(objectclass=*)" * * \param string $basedn Empty string * * \param array $attrs */ function ls ($srp, $filter = "(objectclass=*)", $basedn = "", $attrs = ["*"]) { trigger_error('deprecated'); $this->cd($basedn); return $this->search($srp, $filter, $attrs, 'one'); } /* * \brief Concatenate * * \param integer $srp * * \param string $dn The DN * * \param array $attrs * * \param string $filter Initialized at "(objectclass=*)" */ function cat ($srp, $dn, $attrs = ["*"], $filter = "(objectclass=*)") { if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $this->clearResult($srp); $this->sr[$srp] = @ldap_read($this->cid, $dn, $filter, $attrs); $this->error = @ldap_error($this->cid); $this->resetResult($srp); $this->hasres[$srp] = TRUE; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'cat(dn="'.$dn.'",filter="'.$filter.'")'); return $this->sr[$srp]; } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'cat(dn="'.$dn.'",filter="'.$filter.'")'); return ""; } } /*! * \brief Search object from a filter * * \param string $dn The DN * * \param string $filter The filter of the research */ function object_match_filter ($dn, $filter) { if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $res = @ldap_read($this->cid, $dn, $filter, ["objectClass"]); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'object_match_filter(dn="'.$dn.'",filter="'.$filter.'")'); return @ldap_count_entries($this->cid, $res); } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'object_match_filter(dn="'.$dn.'",filter="'.$filter.'")');
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
return FALSE; } } /*! * \brief Set a size limit * * \param $size The limit */ function set_size_limit ($size) { /* Ignore zero settings */ if ($size == 0) { @ldap_set_option($this->cid, LDAP_OPT_SIZELIMIT, 10000000); } if ($this->hascon) { @ldap_set_option($this->cid, LDAP_OPT_SIZELIMIT, $size); } else { $this->error = "Could not connect to LDAP server"; } logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $size, 'set_size_limit'); } /*! * \brief Fetch * * \param integer $srp * \param bool $cleanUpNumericIndices whether to remove numeric indices and "count" index at top level ("count" index in each attribute value is kept) */ function fetch ($srp, bool $cleanUpNumericIndices = FALSE) { if ($this->hascon) { if ($this->hasres[$srp]) { if ($this->start[$srp] == 0) { if ($this->sr[$srp]) { $this->start[$srp] = 1; $this->re[$srp] = @ldap_first_entry($this->cid, $this->sr[$srp]); } else { return []; } } else { $this->re[$srp] = @ldap_next_entry($this->cid, $this->re[$srp]); } $att = []; if ($this->re[$srp]) { $att = @ldap_get_attributes($this->cid, $this->re[$srp]); $att['dn'] = trim(@ldap_get_dn($this->cid, $this->re[$srp])); if ($cleanUpNumericIndices && isset($att['count'])) { for ($i = 0; $i < $att['count']; ++$i) { /* Remove numeric keys */ unset($att[$i]); } unset($att['count']); } } $this->error = @ldap_error($this->cid); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'fetch()'); return $att; } else { $this->error = "Perform a fetch with no search"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'fetch()'); return ""; } } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'fetch()'); return ""; } }
561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
/*! * \brief Reset the result * * \param integer $srp Value to be reset */ function resetResult ($srp) { $this->start[$srp] = 0; } /*! * \brief Clear a result * * \param integer $srp The result to clear */ function clearResult ($srp) { if ($this->hasres[$srp]) { $this->hasres[$srp] = FALSE; @ldap_free_result($this->sr[$srp]); } } /*! * \brief Accessor of the DN * * \param $srp srp */ function getDN ($srp) { if ($this->hascon) { if ($this->hasres[$srp]) { if (!$this->re[$srp]) { $this->error = "Perform a Fetch with no valid Result"; } else { $rv = @ldap_get_dn($this->cid, $this->re[$srp]); $this->error = @ldap_error($this->cid); return trim($rv); } } else { $this->error = "Perform a Fetch with no Search"; return ""; } } else { $this->error = "Could not connect to LDAP server"; return ""; } } /*! * \brief Return the numbers of entries * * \param $srp srp */ function count ($srp) { if ($this->hascon) { if ($this->hasres[$srp]) { $rv = @ldap_count_entries($this->cid, $this->sr[$srp]); $this->error = @ldap_error($this->cid); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'count()'); return $rv; } else { $this->error = "Perform a Fetch with no Search"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'count()'); return ""; } } else { $this->error = "Could not connect to LDAP server";
631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'count()'); return ""; } } /*! * \brief Remove * * \param string $attrs Empty string * * \param string $dn Empty string */ function rm ($attrs = "", $dn = "") { if ($this->hascon) { if ($this->reconnect) { $this->connect(); } if ($dn == '') { $dn = $this->basedn; } $r = ldap_mod_del($this->cid, $dn, $attrs); $this->error = @ldap_error($this->cid); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rm('.$dn.')'); return $r; } else { $this->error = 'Could not connect to LDAP server'; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rm('.$dn.')'); return ''; } } function mod_add ($attrs = "", $dn = "") { if ($this->hascon) { if ($this->reconnect) { $this->connect(); } if ($dn == "") { $dn = $this->basedn; } $r = @ldap_mod_add($this->cid, $dn, $attrs); $this->error = @ldap_error($this->cid); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'mod_add('.$dn.')'); return $r; } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'mod_add('.$dn.')'); return ""; } } /*! * \brief Remove directory * * \param string $deletedn The DN to be deleted */ function rmdir ($deletedn) { if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $r = @ldap_delete($this->cid, $deletedn); $this->error = @ldap_error($this->cid); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rmdir('.$deletedn.')'); return ($r ? $r : 0);
701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
} else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rmdir('.$deletedn.')'); return ""; } } /*! * \brief Move the given Ldap entry from $source to $dest * * \param String $source The source dn. * * \param String $dest The destination dn. * * \return Boolean TRUE on success else FALSE. */ function rename_dn ($source, $dest) { /* Check if source and destination are the same entry */ if (strtolower($source) == strtolower($dest)) { trigger_error("Source and destination can't be the same entry."); $this->error = "Source and destination can't be the same entry."; return FALSE; } /* Check if destination entry exists */ if ($this->dn_exists($dest)) { trigger_error("Destination '$dest' already exists."); $this->error = "Destination '$dest' already exists."; return FALSE; } /* Extract the name and the parent part out ouf source dn. e.g. cn=herbert,ou=department,dc=... parent => ou=department,dc=... dest_rdn => cn=herbert */ $parent = preg_replace("/^[^,]+,/", "", $dest); $dest_rdn = preg_replace("/,.*$/", "", $dest); if ($this->hascon) { if ($this->reconnect) { $this->connect(); } /* We have to pass TRUE as deleteoldrdn in case the attribute is single-valued */ $r = ldap_rename($this->cid, $source, $dest_rdn, $parent, TRUE); $this->error = ldap_error($this->cid); /* Check if destination dn exists, if not the server may not support this operation */ $r &= $this->dn_exists($dest); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rename("'.$source.'","'.$dest.'")'); return $r; } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rename("'.$source.'","'.$dest.'")'); return FALSE; } } /*! * \brief Function rmdir_recursive * * Based on recursive_remove, adding two thing: full subtree remove, and delete own node. * * \param $srp srp * * \param string $deletedn The dn to delete *
771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
* \return TRUE on sucessfull , 0 in error, and "" when we don't get a ldap conection */ function rmdir_recursive ($srp, $deletedn) { if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $delarray = []; /* Get sorted list of dn's to delete */ $this->cd($deletedn); $this->search($srp, '(objectClass=*)', ['dn']); while ($attrs = $this->fetch($srp)) { $delarray[$attrs['dn']] = strlen($attrs['dn']); } arsort($delarray); reset($delarray); /* Really Delete ALL dn's in subtree */ $r = TRUE; foreach (array_keys($delarray) as $key) { $r = @ldap_delete($this->cid, $key); if ($r === FALSE) { break; } } $this->error = @ldap_error($this->cid); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rmdir_recursive("'.$deletedn.'")'); return ($r ? $r : 0); } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'rmdir_recursive("'.$deletedn.'")'); return ""; } } function makeReadableErrors ($error, $attrs) { if ($this->success()) { return ""; } $str = ""; if (isset($attrs['objectClass']) && preg_match("/^objectClass: value #([0-9]*) invalid per syntax$/", $this->get_additional_error(), $m)) { $ocs = $attrs['objectClass']; if (!is_array($ocs)) { $ocs = [$ocs]; } if (isset($ocs[$m[1]])) { $str .= " - <b>objectClass: ".$ocs[$m[1]]."</b>"; } } if ($error == "Undefined attribute type") { $str = " - <b>attribute: ".preg_replace("/:.*$/", "", $this->get_additional_error())."</b>"; } logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $attrs, "Erroneous data"); return $str; } /*! * \brief Modify a entry of the directory LDAP * * \param array $attrs The new entry */ function modify (array $attrs) {
841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910
if (count($attrs) == 0) { return 0; } if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $r = @ldap_modify($this->cid, $this->basedn, $attrs); $this->error = @ldap_error($this->cid); if (!$this->success()) { $this->error .= $this->makeReadableErrors($this->error, $attrs); } logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'modify('.$this->basedn.')'); return ($r ? $r : 0); } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'modify('.$this->basedn.')'); return ""; } } /*! * \brief Modify a entry of the directory LDAP with fine control * * \param array $changes The changes */ function modify_batch (array $changes) { if (count($changes) == 0) { return TRUE; } if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $r = @ldap_modify_batch($this->cid, $this->basedn, $changes); $this->error = @ldap_error($this->cid); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'modify_batch('.$this->basedn.')'); return $r; } else { $this->error = 'Could not connect to LDAP server'; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'modify_batch('.$this->basedn.')'); return FALSE; } } /*! * \brief Add entry in the LDAP directory * * \param string $attrs The entry to add */ function add ($attrs) { if ($this->hascon) { if ($this->reconnect) { $this->connect(); } $r = @ldap_add($this->cid, $this->basedn, $attrs); $this->error = @ldap_error($this->cid); if (!$this->success()) { $this->error .= $this->makeReadableErrors($this->error, $attrs); } logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'add('.$this->basedn.')'); return ($r ? $r : 0); } else { $this->error = "Could not connect to LDAP server"; logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->error, 'add('.$this->basedn.')'); return ""; } }
911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
/* * $target is a dn, i.e. "ou=example,ou=orga,dc=base" * * Creates missing trees, in our example ou=orga,dc=base will get created if not existing, same thing for ou=example,ou=orga,dc=base * */ function create_missing_trees ($srp, $target, $ignoreReferralBases = TRUE) { $real_path = substr($target, 0, strlen($target) - strlen($this->basedn) - 1); if ($target == $this->basedn) { $l = ["dummy"]; } else { $l = array_reverse(ldap_explode_dn($real_path, 0)); } unset($l['count']); $cdn = $this->basedn; /* Load schema if available... */ $classes = $this->get_objectclasses(); foreach ($l as $part) { if ($part != "dummy") { $cdn = "$part,$cdn"; } /* Ignore referrals */ if ($ignoreReferralBases) { $found = FALSE; foreach ($this->referrals as $ref) { if ($ref['BASE'] == $cdn) { $found = TRUE; break; } } if ($found) { continue; } } /* Create missing entry? */ if (!$this->dn_exists($cdn)) { $type = preg_replace('/^([^=]+)=.*$/', '\\1', $cdn); $param = preg_replace('/^[^=]+=([^,]+).*$/', '\\1', $cdn); $param = preg_replace(['/\\\\,/','/\\\\"/'], [',','"'], $param); $na = []; /* Automatic or traditional? */ if (count($classes)) { if ($type == 'l') { /* Locality has l as MAY so autodetection fails */ $ocname = 'locality'; } else { /* Get name of first matching objectClass */ $ocname = ''; foreach ($classes as $class) { if (isset($class['MUST']) && in_array($type, $class['MUST'])) { /* Look for first classes that is structural... */ if (isset($class['STRUCTURAL'])) { $ocname = $class['NAME']; break; } /* Look for classes that are auxiliary... */ if (isset($class['AUXILIARY'])) { $ocname = $class['NAME']; } } }
981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
} /* Bail out, if we've nothing to do... */ if ($ocname == '') { throw new FusionDirectoryError(htmlescape(sprintf(_('Cannot automatically create subtrees with RDN "%s": no object class found!'), $type))); } /* Assemble_entry */ $na['objectClass'] = [$ocname]; if (isset($classes[$ocname]['AUXILIARY'])) { $na['objectClass'][] = $classes[$ocname]['SUP']; } if ($type == 'dc') { /* This is bad actually, but - tell me a better way? */ $na['objectClass'][] = 'organization'; $na['o'] = $param; } $na[$type] = $param; // Fill in MUST values - but do not overwrite existing ones. $oc = $ocname; do { if (isset($classes[$oc]['MUST']) && is_array($classes[$oc]['MUST'])) { foreach ($classes[$oc]['MUST'] as $attr) { if (isset($na[$attr]) && !empty($na[$attr])) { continue; } $na[$attr] = 'filled'; } } $oc = ($classes[$oc]['SUP'] ?? NULL); } while ($oc); } else { /* Use alternative add... */ switch ($type) { case 'ou': $na['objectClass'] = 'organizationalUnit'; $na['ou'] = $param; break; case 'dc': $na['objectClass'] = ['dcObject', 'top', 'organization']; $na['dc'] = $param; $na['o'] = $param; break; default: throw new FusionDirectoryError(htmlescape(sprintf(_('Cannot automatically create subtrees with RDN "%s": not supported'), $type))); } } $this->cd($cdn); $this->add($na); if (!$this->success()) { logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $cdn, 'dn'); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $na, 'Content'); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $this->get_error(), 'LDAP error'); throw new FusionDirectoryLdapError($cdn, LDAP_ADD, $this->get_error(), $this->get_errno()); } } } } /*! * \brief Get the LDAP additional error * * \return string containts LDAP_OPT_ERROR_STRING */ function get_additional_error () {
1051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
$additional_error = ''; @ldap_get_option($this->cid, LDAP_OPT_ERROR_STRING, $additional_error); return $additional_error; } /*! * \brief Success * * \return boolean TRUE if Success is found in $error, else return FALSE */ function success (): bool { return (trim($this->error) === 'Success'); } /*! * \brief Get the error */ function get_error ($details = TRUE): string { if (($this->error == 'Success') || !$details) { return $this->error; } else { $adderror = $this->get_additional_error(); if ($adderror != '') { return sprintf( _('%s (%s, while operating on "%s" using LDAP server "%s")'), $this->error, $adderror, $this->basedn, $this->hostname ); } else { return sprintf( _('%s (while operating on LDAP server "%s")'), $this->error, $this->hostname ); } } } /*! * \brief Get the errno * * Must be run right after the ldap request */ function get_errno (): int { if ($this->error == 'Success') { return 0; } else { return @ldap_errno($this->cid) ?? -1; } } /*! * \brief Check if the search hit the size limit * * Must be run right after the search */ function hitSizeLimit (): bool { /* LDAP_SIZELIMIT_EXCEEDED 0x04 */ return ($this->get_errno() == 0x04); } function get_credentials ($url, $referrals = NULL) { $ret = []; $url = preg_replace('!\?\?.*$!', '', $url); $server = preg_replace('!^([^:]+://[^/]+)/.*$!', '\\1', $url); if ($referrals === NULL) {
1121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
$referrals = $this->referrals; } if (isset($referrals[$server])) { return $referrals[$server]; } else { $ret['ADMINDN'] = $this->binddn; $ret['ADMINPASSWORD'] = $this->bindpw; } return $ret; } /*! * \brief Generates an ldif for all entries matching the filter settings, scope and limit. * * \param string $dn The entry to export. * * \param string $filter Limit the exported object to those maching this filter. * * \param string $scope 'base', 'sub' .. see manpage for 'ldapmodify' for details. * * \param int $limit Limits the result. * * \param ?int $wrap Wraps line around this length (0 to disable). */ function generateLdif (string $dn, string $filter = '(objectClass=*)', string $scope = 'sub', int $limit = 0, int $wrap = NULL): string { $limit = (($limit == 0) ? '' : ' -z '.$limit); if ($wrap === NULL) { $wrap = ''; } else { $wrap = ' -o ldif-wrap='.($wrap ? $wrap : 'no'); } // Check scope values $scope = trim($scope); if (!empty($scope) && !in_array($scope, ['base', 'one', 'sub', 'children'])) { throw new LDIFExportException(sprintf('Invalid parameter for scope "%s", please use "base", "one", "sub" or "children".', $scope)); } $scope = (empty($scope) ? '' : ' -s '.$scope); // Prepare parameters to be valid for shell execution $dn = escapeshellarg($dn); $pwd = escapeshellarg($this->bindpw); $host = escapeshellarg($this->hostname); $admin = escapeshellarg($this->binddn); $filter = escapeshellarg($filter); $cmd = 'ldapsearch'.($this->tls ? ' -ZZ' : '')." -x -LLLL -D {$admin} {$filter} {$limit} {$wrap} {$scope} -H {$host} -b {$dn} -w {$pwd} "; // Create list of process pipes $descriptorspec = [ 0 => ["pipe", "r"], // stdin 1 => ["pipe", "w"], // stdout 2 => ["pipe", "w"] // stderr ]; // Try to open the process $process = proc_open($cmd, $descriptorspec, $pipes); if ($process !== FALSE) { // Write the password to stdin fclose($pipes[0]); // Get results from stdout and stderr $res = stream_get_contents($pipes[1]); $err = stream_get_contents($pipes[2]); fclose($pipes[1]);
1191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
// Close the process and check its return value if (proc_close($process) != 0) { throw new LDIFExportException($err); } } else { throw new LDIFExportException(_('proc_open failed to execute ldapsearch')); } return $res; } function dn_exists ($dn): bool { logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, '', 'dn_exists('.$dn.')'); return (@ldap_read($this->cid, $dn, '(objectClass=*)', ['objectClass']) !== FALSE); } function parseLdif (string $str_attr): array { /* First we split the string into lines */ $fileLines = preg_split("/\n/", $str_attr); if (end($fileLines) != '') { $fileLines[] = ''; } /* Joining lines */ $line = NULL; $entry = []; $entries = []; $entryStart = -1; foreach ($fileLines as $lineNumber => $fileLine) { if (preg_match('/^ /', $fileLine)) { if ($line === NULL) { throw new LDIFImportException(sprintf(_('Error line %s, first line of an entry cannot start with a space'), $lineNumber)); } /* Append to current line */ $line .= substr($fileLine, 1); } else { if ($line !== NULL) { if (preg_match('/^#/', $line) || (preg_match('/^version:/', $line) && empty($entry))) { /* Ignore comment */ /* Ignore version number */ } else { /* Line has ended */ list ($key, $value) = explode(':', $line, 2); $value = trim($value); if (preg_match('/^:/', $value)) { $value = base64_decode(trim(substr($value, 1))); } if (preg_match('/^</', $value)) { throw new LDIFImportException(sprintf(_('Error line %s, references to an external file are not supported'), $lineNumber)); } if ($value === '') { throw new LDIFImportException(sprintf(_('Error line %s, attribute "%s" has no value'), $lineNumber, $key)); } if ($key == 'dn') { if (!empty($entry)) { throw new LDIFImportException(sprintf(_('Error line %s, an entry bloc can only have one dn'), $lineNumber)); } $entry['dn'] = $value; $entryStart = $lineNumber; } elseif (empty($entry)) { throw new LDIFImportException(sprintf(_('Error line %s, an entry bloc should start with the dn'), $lineNumber)); } else { if (!isset($entry[$key])) { $entry[$key] = []; } $entry[$key][] = $value; } }
1261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330
} /* Start new line */ $line = trim($fileLine); if ($line == '') { if (!empty($entry)) { /* Entry is finished */ $entries[$entryStart] = $entry; } /* Start a new entry */ $entry = []; $entryStart = -1; $line = NULL; } } } return $entries; } /*! * \brief Function to imports ldifs * * If DeleteOldEntries is TRUE, the destination entry will be deleted first. * If JustModify is TRUE the destination entry will only be touched by the attributes specified in the ldif. * if JustMofify is FALSE the destination dn will be overwritten by the new ldif. * * \param integer $srp * * \param string $str_attr * * \param boolean $JustModify * * \param boolean $DeleteOldEntries */ function import_complete_ldif ($srp, $str_attr, $JustModify, $DeleteOldEntries) { $entries = $this->parseLdif($str_attr); if ($this->reconnect) { $this->connect(); } foreach ($entries as $startLine => $entry) { /* Delete before insert */ $usermdir = ($this->dn_exists($entry['dn']) && $DeleteOldEntries); /* Should we use Modify instead of Add */ $usemodify = ($this->dn_exists($entry['dn']) && $JustModify); /* If we can't Import, return with a file error */ if (!$this->import_single_entry($srp, $entry, $usemodify, $usermdir)) { throw new LDIFImportException(sprintf(_('Error while importing dn: "%s", please check your LDIF from line %s on!'), $entry['dn'][0], $startLine)); } } return count($entries); } /*! \brief Function to Imports a single entry * * If $delete is TRUE; The old entry will be deleted if it exists. * if $modify is TRUE; All variables that are not touched by the new ldif will be kept. * if $modify is FALSE; The new ldif overwrites the old entry, and all untouched attributes get lost. * * \param integer $srp * * \param array $data * * \param boolean $modify * * \param boolean $delete
1331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400
*/ protected function import_single_entry ($srp, $data, $modify, $delete) { global $config; if (!$config) { trigger_error("Can't import ldif, can't read config object."); } if ($this->reconnect) { $this->connect(); } $ret = FALSE; $dn = NULL; $operation = NULL; /* If dn is an index of data, we should try to insert the data */ if (isset($data['dn'])) { /* Fix dn */ $tmp = ldap_explode_dn($data['dn'], 0); unset($tmp['count']); $dn = ''; foreach ($tmp as $tm) { $dn .= trim($tm).','; } $dn = preg_replace('/,$/', '', $dn); unset($data['dn']); /* Creating Entry */ $this->cd($dn); /* Delete existing entry */ if ($delete) { $this->rmdir_recursive($srp, $dn); } /* Create missing trees */ $this->cd($config->current['BASE']); try { $this->create_missing_trees($srp, preg_replace('/^[^,]+,/', '', $dn)); } catch (FusionDirectoryError $error) { $error->display(); } $this->cd($dn); $operation = LDAP_MOD; if (!$modify) { $this->cat($srp, $dn); if ($this->count($srp)) { /* The destination entry exists, overwrite it with the new entry */ $attrs = $this->fetch($srp); foreach (array_keys($attrs) as $name) { if (!is_numeric($name)) { if (in_array($name, ['dn','count'])) { continue; } if (!isset($data[$name])) { $data[$name] = []; } } } $ret = $this->modify($data); } else { /* The destination entry doesn't exists, create it */ $operation = LDAP_ADD; $ret = $this->add($data); } } else { /* Keep all vars that aren't touched by this ldif */
1401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470
$ret = $this->modify($data); } } if (!$this->success()) { $error = new FusionDirectoryLdapError($dn, $operation, $this->get_error(), $this->get_errno()); $error->display(); } return $ret; } /*! * \brief Get the object classes * * \param boolean $force_reload FALSE */ function get_objectclasses ($force_reload = FALSE) { /* Return the cached results. */ if (class_available('session') && session::is_set('LDAP_CACHE::get_objectclasses') && !$force_reload) { return session::get('LDAP_CACHE::get_objectclasses'); } // Get base to look for schema $res = @ldap_read($this->cid, '', 'objectClass=*', ['subschemaSubentry']); $attrs = @ldap_get_entries($this->cid, $res); if (!isset($attrs[0]['subschemasubentry'][0])) { return []; } /* Get list of objectclasses and fill array */ $nb = $attrs[0]['subschemasubentry'][0]; $objectclasses = []; $res = ldap_read($this->cid, $nb, 'objectClass=*', ['objectclasses']); $attrs = ldap_get_entries($this->cid, $res); if (!isset($attrs[0])) { return []; } foreach ($attrs[0]['objectclasses'] as $val) { if (preg_match('/^[0-9]+$/', $val)) { continue; } $name = 'OID'; $pattern = explode(' ', $val); $ocname = preg_replace("/^.* NAME\s+\(*\s*'([^']+)'\s*\)*.*$/", '\\1', $val); $objectclasses[$ocname] = []; $value = ''; foreach ($pattern as $chunk) { switch ($chunk) { case '(': $value = ''; break; case ')': if ($name != '') { $v = $this->value2container($value); if (in_array($name, ['MUST', 'MAY']) && !is_array($v)) { $v = [$v]; } $objectclasses[$ocname][$name] = $v; } $name = ''; $value = ''; break; case 'NAME':
1471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540
case 'DESC': case 'SUP': case 'STRUCTURAL': case 'ABSTRACT': case 'AUXILIARY': case 'MUST': case 'MAY': if ($name != '') { $v = $this->value2container($value); if (in_array($name, ['MUST','MAY']) && !is_array($v)) { $v = [$v]; } $objectclasses[$ocname][$name] = $v; } $name = $chunk; $value = ''; break; default: $value .= $chunk.' '; } } } if (class_available('session')) { session::set('LDAP_CACHE::get_objectclasses', $objectclasses); } return $objectclasses; } function value2container ($value) { /* Set emtpy values to "TRUE" only */ if (preg_match('/^\s*$/', $value)) { return TRUE; } /* Remove ' and " if needed */ $value = preg_replace('/^[\'"]/', '', $value); $value = preg_replace('/[\'"] *$/', '', $value); /* Convert to array if $ is inside... */ if (preg_match('/\$/', $value)) { $container = preg_split('/\s*\$\s*/', $value); } else { $container = chop($value); } return $container; } /*! * \brief Add a string in log file * * \param string $string */ function log ($string) { if (session::is_set('config')) { $cfg = session::get('config'); if (isset($cfg->current['LDAPSTATS']) && preg_match('/true/i', $cfg->current['LDAPSTATS'])) { syslog(LOG_INFO, $string); } } } /* added by Guido Serra aka Zeph <zeph@purotesto.it> */ /*! * \brief Function to get cn
15411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575
* * \param $dn The DN */ function getCn ($dn) { $simple = explode(",", $dn); foreach ($simple as $piece) { $partial = explode("=", $piece); if ($partial[0] == "cn") { return $partial[1]; } } } public static function get_naming_contexts ($server, $admin = '', $password = '') { /* Build LDAP connection */ $ds = ldap_connect($server); if (!$ds) { die('Can\'t bind to LDAP. No check possible!'); } ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_bind($ds, $admin, $password); /* Get base to look for naming contexts */ $res = @ldap_read($ds, '', 'objectClass=*', ['namingContexts']); $attrs = @ldap_get_entries($ds, $res); logging::debug(DEBUG_LDAP, __LINE__, __FUNCTION__, __FILE__, $attrs[0]['namingcontexts'], 'get_naming_contexts'); return $attrs[0]['namingcontexts']; } }