*/ class LSldap extends LSlog_staticLoggerClass { /** * LDAP connection configuration * (LSconfig.ldap_servers..ldap_config) * @see LSsession::LSldapConnect() * @see LSldap::setConfig() * @see LSldap::getConfig() * @var array */ private static $config = array(); /** * LDAP connection (Net_LDAP2 object) * @see LSldap::connect() * @see LSldap::reconnectAs() * @see LSldap::isConnected() * @see LSldap::close() * @var Net_LDAP2|null */ private static $cnx = NULL; /** * Registered events * @see self::addEvent() * @see self::fireEvent() * @var array */ private static $_events = array(); /** * Set configuration * * This method permit to define LDAP server access configuration * * @author Benjamin Renard * * @param array $config Configuration array as accepted by Net_LDAP2 * * @return void */ public static function setConfig ($config) { self :: $config = $config; } /** * Connect to LDAP server * * This method establish connection to LDAP server * * @author Benjamin Renard * * @param array|null $config LDAP configuration array in format of Net_LDAP2 * * @return boolean true if connected, false instead */ public static function connect($config=null) { if ($config) { self :: setConfig($config); } if (!self :: fireEvent('connecting')) return false; self :: $cnx = Net_LDAP2::connect(self :: $config); if (Net_LDAP2::isError(self :: $cnx)) { self :: fireEvent('connection_failure', array('error' => self :: $cnx -> getMessage())); LSerror :: addErrorCode('LSldap_01',self :: $cnx -> getMessage()); self :: $cnx = NULL; return false; } self :: fireEvent('connected'); return true; } /** * Reconnect (or connect) with other credentials * * @author Benjamin Renard * * @param string $dn Bind DN * @param string $pwd Bind password * @param array|null $config LDAP configuration array as expected by Net_LDAP2 * (optional, default: keep current) * * @return boolean true if connected, false instead */ public static function reconnectAs($dn, $pwd, $config=null) { if ($config) { self :: setConfig($config); } if (self :: $cnx) { self :: $cnx -> done(); } $config = self :: $config; $config['binddn'] = $dn; $config['bindpw'] = $pwd; if (!self :: fireEvent('reconnecting', array('dn' => $dn))) return false; self :: $cnx = Net_LDAP2::connect($config); if (Net_LDAP2::isError(self :: $cnx)) { self :: fireEvent( 'reconnection_failure', array('dn' => $dn, 'error' => self :: $cnx -> getMessage()) ); LSerror :: addErrorCode('LSldap_01', self :: $cnx -> getMessage()); self :: $cnx = NULL; return false; } self :: fireEvent('reconnected', array('dn' => $dn)); return true; } /** * Set authz proxy control * * @author Benjamin Renard * * @param string $dn Bind DN * * @return boolean true if authz proxy controle is set, false otherwise */ public static function setAuthzProxyControl($dn) { if (!self :: $cnx) { self :: connect(); } if (!self :: fireEvent('setting_authz_proxy', array('dn' => $dn))) return false; $result = self :: $cnx -> setOption( 'LDAP_OPT_SERVER_CONTROLS', array ( array( 'oid' => '2.16.840.1.113730.3.4.18', 'value' => "dn:$dn", 'iscritical' => true ) ) ); // Also check user exists to validate the connection with // authz proxy control. if ($result !== True || !self :: exists($dn)) { self :: fireEvent('setting_authz_proxy_failure', array('dn' => $dn)); LSerror :: addErrorCode('LSldap_09'); return False; } self :: fireEvent('set_authz_proxy', array('dn' => $dn)); return True; } /** * Disconnect * * This method permit to close the connection to the LDAP server * * @author Benjamin Renard * * @return void */ public static function close() { if (!self :: fireEvent('closing')) return; self :: $cnx -> done(); self :: $cnx = null; self :: fireEvent('closed'); } /** * Search in LDAP directory * * This method make a search in LDAP directory and return the result as an array. * * @author Benjamin Renard * * @param string $filter The search LDAP filter * @param string $basedn The base DN of the search * @param array $params Array to search parameters as accepted by Net_LDAP2::search() * * @see Net_LDAP2::search() * * @return array|false Return an array of entries returned by the LDAP directory. * Each element of this array corresponded to one returned entry and is * an array with the following keys: * - dn: The DN of the entry * - attrs: Associative array of the entry's attributes values * False returned in case of error. */ public static function search($filter, $basedn=NULL, $params=array()) { $filterstr = (is_a($filter, 'Net_LDAP2_Filter')?$filter->asString():$filter); if (is_empty($basedn)) { $basedn = self :: getConfig('basedn'); if (is_empty($basedn)) { LSerror :: addErrorCode('LSldap_08'); return false; } self :: log_debug("LSldap::search($filterstr): empty basedn provided, use basedn from configuration: ".varDump($basedn)); } self :: log_trace("LSldap::search($filterstr, $basedn): run search with parameters: ".varDump($params)); $ret = self :: $cnx -> search($basedn, $filter, $params); if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode('LSldap_02', $ret -> getMessage()); return false; } self :: log_debug("LSldap::search($filterstr, $basedn) : return ".$ret->count()." objet(s)"); $retInfos = array(); foreach($ret as $dn => $entry) { if (!$entry instanceof Net_LDAP2_Entry) { LSerror :: addErrorCode('LSldap_02', "LDAP search return an ".get_class($entry).". object"); continue; } $retInfos[] = array( 'dn' => $dn, 'attrs' => $entry -> getValues() ); } return $retInfos; } /** * Count the number of mathching objects found in LDAP directory * * This method make a search in LDAP directory and return the number of * macthing entries. * * @author Benjamin Renard * * @param string $filter The search LDAP filter * @param string $basedn The base DN of the search * @param array $params Array to search parameters as accepted by Net_LDAP2::search() * * @see Net_LDAP2::search() * * @return integer|null The number of matching entries on success, null otherwise */ public static function getNumberResult($filter, $basedn=NULL, $params=array()) { if (empty($filter)) $filter = NULL; $filterstr = (is_a($filter, 'Net_LDAP2_Filter')?$filter->asString():$filter); if (is_empty($basedn)) { $basedn = self :: getConfig('basedn'); if (is_empty($basedn)) { LSerror :: addErrorCode('LSldap_08'); return null; } self :: log_debug("LSldap::getNumberResult($filterstr): empty basedn provided, use basedn from configuration: ".varDump($basedn)); } self :: log_trace("LSldap::getNumberResult($filterstr, $basedn): run search with parameters: ".varDump($params)); $ret = self :: $cnx -> search($basedn, $filter, $params); if (Net_LDAP2::isError($ret)) { LSerror :: addErrorCode('LSldap_02',$ret -> getMessage()); return null; } $count = $ret -> count(); self :: log_trace("LSldap::getNumberResult($filterstr, $basedn): result=$count"); return $count; } /** * Load values of an LDAP entry attributes * * This method retrieve attributes values of an LDAP entry and return it * as associative array. * * @author Benjamin Renard * * @param string $dn DN de l'entré Ldap * @param string|Net_LDAP2_Filter|null $filter LDAP filter string (optional, default: null == '(objectClass=*)') * @param array|null $attrs Array of requested attribute (optional, default: null == all attributes, excepted internal) * @param boolean $include_internal If true, internal attributes will be included (default: false) * * @return array|false Associative array of attributes values (with attribute name as key), or false on error */ public static function getAttrs($dn, $filter=null, $attrs=null, $include_internal=false) { $infos = ldap_explode_dn($dn,0); if((!$infos)||($infos['count']==0)) return false; if (!$filter) $filter = '(objectClass=*)'; $params = array( 'scope' => 'base', 'attributes' => (is_array($attrs)?$attrs:array('*')), ); if ($include_internal && !in_array('+', $params['attributes'])) $params['attributes'][] = '+'; $return = self :: search($filter, $dn, $params); if (is_array($return) && count($return) == 1) return $return[0]['attrs']; return false; } /** * Parse a date string as Datetime object * * @param string $value LDAP date string to parse * * @return Datetime|false Datetime object, or false */ public static function parseDate($value) { $datetime = date_create_from_format('YmdHis.uO', $value); return ( $datetime instanceof DateTime? $datetime -> setTimezone(timezone_open(date_default_timezone_get())): false ); } /** * Check if an attribute exists in specified attributes collection * * It performs a case-insensitive search. * * @author Emmanuel Saracco * * @param array $attrs Array of LDAP attributes * @param string $name Name of a attribute * * @return boolean true if found */ public static function attrExists($attrs, $name) { return array_key_exists(strtolower($name), array_change_key_case($attrs)); } /** * Return a attribute value * * It performs a case-insensitive search. * * @author Emmanuel Saracco * * @param array $attrs Array of LDAP attributes * @param string $name Name of a attribute * @param boolean $multiple true if we must return array * * @return ($multiple is True ? array : string|null) Found value (or array of values) or null */ public static function getAttr($attrs, $name, $multiple=false) { $name = strtolower($name); foreach ($attrs as $k => $v) { if (strtolower($k) === $name) { $v = ensureIsArray($v); return $multiple ? $v : $v[0]; } } return $multiple ? array() : null; } /** * Return an existing or new LDAP entry * * @author Benjamin Renard * * @param string $object_type The object type * @param string $dn The DN of the LDAP entry * * @return Net_LDAP2_Entry|array|false A Net_LDAP2_Entry object or an array if * it's a new entry: * Array ( * 'entry' => Net_LDAP2_Entry, * 'new' => true * ) * False returned in case of error */ public static function getEntry($object_type, $dn) { $obj_classes = LSconfig :: get("LSobjects.$object_type.objectclass"); if(!is_array($obj_classes)){ LSerror :: addErrorCode('LSldap_03'); return false; } $attrs = array_keys(LSconfig :: get("LSobjects.$object_type.attrs", array(), 'array')); $entry = self :: getLdapEntry($dn, $attrs); if ($entry === false) { $newentry = self :: getNewEntry($dn, $obj_classes, array()); if (!$newentry) { return false; } // Mark entry as new $newentry -> markAsNew(); return $newentry; } // Mark entry as NOT new $entry -> markAsNew(false); return $entry; } /** * Return a Net_LDAP2_Entry object of an existing entry * * @author Benjamin Renard * * @param string $dn DN of the requested LDAP entry * @param array|null $attrs Array of requested attribute (optional, default: null == all attributes, excepted internal) * * @return Net_LDAP2_Entry|false A Net_LDAP2_Entry object or false if error occured */ public static function getLdapEntry($dn, $attrs=null) { $entry = self :: $cnx -> getEntry($dn, (is_array($attrs)?$attrs:array())); if (Net_LDAP2::isError($entry)) { return false; } else { return $entry; } } /** * Check if an LDAP object exists * * @author Benjamin Renard * * @param string $dn DN of the LDAP entry to check * * @return boolean True if entry exists, false otherwise */ public static function exists($dn) { return is_a(self :: getLdapEntry($dn), 'Net_LDAP2_Entry'); } /** * Return a new Net_LDAP2_Entry object * * @param string $dn The DN of the object * @param array $objectClass Array of the object's object classes * @param array $attrs Array of the object's attributes values * @param boolean $add Set to true to add the new entry to LDAP directory (default: false) * * @return Net_LDAP2_Entry|False A Net_LDAP2_Entry object on success, False otherwise */ public static function getNewEntry($dn, $objectClass, $attrs, $add=false) { $newentry = Net_LDAP2_Entry::createFresh( $dn, array_merge( array('objectclass' =>$objectClass), ensureIsArray($attrs) ) ); if(Net_LDAP2::isError($newentry)) { return false; } if($add) { if(!self :: $cnx -> add($newentry)) { return false; } } return $newentry; } /** * Update an entry in LDAP * * Note: this method drop empty attribute values and attributes without value. * * @author Benjamin Renard * * @param string $object_type The object type * @param string $dn DN of the LDAP object * @param array $changes Array of object attributes changes * * @return boolean True if object was updated, False otherwise. */ public static function update($object_type, $dn, $changes) { self :: log_trace("update($object_type, $dn): change=".varDump($changes)); // Retrieve current LDAP entry $entry = self :: getEntry($object_type, $dn); if(!is_a($entry, 'Net_LDAP2_Entry')) { LSerror :: addErrorCode('LSldap_04'); return false; } if ( !self :: fireEvent( 'updating', array('object_type' => $object_type, 'dn' => $dn, 'entry' => &$entry, 'changes' => $changes) ) ) return false; // Distinguish drop attributes from change attributes $changed_attrs = array(); $dropped_attrs = array(); foreach($changes as $attrName => $attrVal) { $drop = true; if (is_array($attrVal)) { foreach($attrVal as $val) { if (!is_empty($val)) { $drop = false; $changed_attrs[$attrName][]=$val; } } } else { if (!is_empty($attrVal)) { $drop = false; $changed_attrs[$attrName][]=$attrVal; } } if($drop) { $dropped_attrs[] = $attrName; } } self :: log_trace("update($object_type, $dn): changed attrs=".varDump($changed_attrs)); self :: log_trace("update($object_type, $dn): dropped attrs=".varDump($dropped_attrs)); // Set an error flag to false $error = false; // Handle special case: user password change if ($changed_attrs && !$entry->isNew() && self :: attrExists($changed_attrs, 'userPassword')) { $changed_attrs = self :: updateUserPassword($object_type, $changed_attrs, $dn); if ($changed_attrs === false) { return false; } } // Keep original entry (to provide to hooks) $original_entry = clone $entry; // Handle attributes changes (if need) if ($changed_attrs) { $entry -> replace($changed_attrs); if ($entry -> isNew()) { self :: log_debug("update($object_type, $dn): add new entry"); $ret = self :: $cnx -> add($entry); } else { self :: log_debug("update($object_type, $dn): update entry (for changed attributes)"); $ret = $entry -> update(); } if (Net_LDAP2::isError($ret)) { self :: fireEvent( 'update_failure', array( 'object_type' => $object_type, 'dn' => $dn, 'original_entry' => &$original_entry, 'entry' => &$entry, 'changes' => $changed_attrs, 'error' => $ret->getMessage() ) ); LSerror :: addErrorCode('LSldap_05',$dn); LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return false; } self :: fireEvent( 'updated', array( 'object_type' => $object_type, 'dn' => $dn, 'original_entry' => &$original_entry, 'entry' => &$entry, 'changes' => $changed_attrs ) ); } elseif ($entry -> isNew()) { self :: log_error("update($object_type, $dn): no changed attribute but it's a new entry..."); return false; } else { self :: log_debug("update($object_type, $dn): no changed attribute"); } // Handle droped attributes (is need and not a new entry) if ($dropped_attrs && !$entry -> isNew()) { // $entry -> delete() method is buggy (for some attribute like jpegPhoto) // Prefer replace attribute by an empty array $replace_attrs = array(); foreach($dropped_attrs as $attr) { // Check if attribute is present if(!$entry -> exists($attr)) { // Attribute not present on LDAP entry self :: log_debug("update($object_type, $dn): dropped attr $attr is not present in LDAP entry => ignore it"); continue; } $replace_attrs[$attr] = array(); } if (!$replace_attrs) { self :: log_debug("update($object_type, $dn): no attribute to drop"); return true; } // Replace values in LDAP $entry -> replace($replace_attrs); self :: log_debug("update($object_type, $dn): update entry (for dropped attributes: ".implode(', ', array_keys($replace_attrs)).")"); $ret = $entry -> update(); // Check result if (Net_LDAP2::isError($ret)) { self :: fireEvent( 'update_failure', array( 'object_type' => $object_type, 'dn' => $dn, 'original_entry' => &$original_entry, 'entry' => &$entry, 'changes' => $replace_attrs, 'error' => $ret->getMessage() ) ); LSerror :: addErrorCode('LSldap_06'); LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return false; } self :: fireEvent( 'updated', array( 'object_type' => $object_type, 'dn' => $dn, 'original_entry' => &$original_entry, 'entry' => &$entry, 'changes' => $replace_attrs ) ); } return true; } /** * Test to bind to LDAP directory * * This method establish a connection to the LDAP server and test * to bind with provided DN and password. * * @author Benjamin Renard * * @return boolean True on bind success, False otherwise. */ public static function checkBind($dn,$pwd) { $config = self :: $config; $config['binddn'] = $dn; $config['bindpw'] = $pwd; $cnx = Net_LDAP2::connect($config); if (Net_LDAP2::isError($cnx)) { return false; } return true; } /** * Return the status of the LDAP connection * * @return boolean True if connected on LDAP server, False otherwise */ public static function isConnected() { return (self :: $cnx == NULL)?false:true; } /** * Drop an object in LDAP directory * * @param string $dn The DN of the object to remove * * @return boolean True if object was removed, False otherwise. */ public static function remove($dn) { if (!self :: fireEvent('removing', array('dn' => $dn))) return false; $ret = self :: $cnx -> delete($dn,array('recursive' => true)); if (Net_LDAP2::isError($ret)) { self :: fireEvent('remove_failure', array('dn' => $dn, 'error' => $ret->getMessage())); LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return false; } self :: fireEvent('removed', array('dn' => $dn)); return true; } /** * Move an entry in LDAP directory * * @param string $old The current object DN * @param string $new The new object DN * * @return boolean True if object was moved, False otherwise. */ public static function move($old, $new) { if (!self :: fireEvent('moving', array('old' => $old, 'new' => $new))) return false; $ret = self :: $cnx -> move($old, $new); if (Net_LDAP2::isError($ret)) { self :: fireEvent( 'move_failure', array('old' => $old, 'new' => $new, 'error' => $ret->getMessage()) ); LSerror :: addErrorCode('LSldap_07'); LSerror :: addErrorCode(0,'NetLdap-Error : '.$ret->getMessage()); return false; } self :: fireEvent('moved', array('old' => $old, 'new' => $new)); return true; } /** * Combine LDAP Filters * * @param string $op The combine logical operator. May be "and", * "or", "not" or the subsequent logical * equivalents "&", "|", "!". * @param array[string|Net_LDAP2_Filter] $filters Array of LDAP filters (as string or * Net_LDAP2_Filter object) * @param boolean $asStr Set to true if you want to retreive * combined filter as string instead of * as a Net_LDAP2_Filter object (optional, * default: false) * * @return string|Net_LDAP2_Filter|false The combined filter or False in case of error **/ public static function combineFilters($op, $filters, $asStr=false) { if (is_array($filters) && !empty($filters)) { if (count($filters)==1) { if ($asStr && $filters[0] instanceof Net_LDAP2_Filter) { return $filters[0]->asString(); } else { return $filters[0]; } } $filter=Net_LDAP2_Filter::combine($op,$filters); if (!Net_LDAP2::isError($filter)) { if ($asStr) { return $filter->asString(); } else { return $filter; } } else { LSerror :: addErrorCode(0,$filter -> getMessage()); } } return false; } /** * Check LDAP Filters String * * @param string $filter A LDAP filter as string * * @return boolean True only if the filter could be parsed **/ public static function isValidFilter($filter) { if (is_string($filter) && !empty($filter)) { $filter=Net_LDAP2_Filter::parse($filter); if (!Net_LDAP2::isError($filter)) { return true; } else { LSerror :: addErrorCode(0,$filter -> getMessage()); } } return false; } /** * Update userPassword attribute * * This method uses LDAP controls when possible (Net_LDAP2 does not). * * @param string $object_type The object type * @param array $changed_attrs Array of changed attributes * @param string $dn DN of the LDAP object * * @author Emmanuel Saracco * * @return array|false New array of changed attributes or false **/ private static function updateUserPassword($object_type, $changed_attrs, $dn) { if (self :: getConfig('version') < 3 || !function_exists('ldap_mod_replace_ext')) { return $changed_attrs; } $ppolicyErrorMsg = array( _('The password expired'), _('The account is locked'), _('The password was reset and must be changed'), _('It is not possible to modify the password'), _('The old password must be supplied'), _('The password does not meet the quality requirements'), _('The password is too short'), _('It is too soon to change the password'), _('This password was recently used and cannot be used again'), ); self :: log_debug("updateUserPassword($object_type, $dn): update entry userPassword attribute"); $changes = array('userPassword' => self :: getAttr($changed_attrs, 'userPassword', true)); if ( !self :: fireEvent( 'user_password_updating', array( 'object_type' => $object_type, 'dn' => $dn, 'new_passwords' => $changes['userPassword'] ) ) ) return false; $ldap = self :: $cnx->getLink(); $ctrlRequest = array(array('oid' => LDAP_CONTROL_PASSWORDPOLICYREQUEST)); $r = ldap_mod_replace_ext($ldap, $dn, $changes, $ctrlRequest); if ($r && ldap_parse_result($ldap, $r, $errcode, $matcheddn, $errmsg, $ref, $ctrlResponse)) { if ($errcode !== 0 && isset($ctrlResponse[LDAP_CONTROL_PASSWORDPOLICYRESPONSE])) { self :: fireEvent( 'user_password_update_failure', array( 'object_type' => $object_type, 'dn' => $dn, 'error' => $ppolicyErrorMsg[ $ctrlResponse[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error'] ] ) ); LSerror :: addErrorCode( 'LSldap_10', $ppolicyErrorMsg[$ctrlResponse[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error']] ); return false; } // Password updated self :: fireEvent( 'user_password_updated', array( 'object_type' => $object_type, 'dn' => $dn, 'new_passwords' => $changes['userPassword'] ) ); // Remove userPassword to prevent it from being processed by update() unset($changed_attrs['userPassword']); } else { self :: fireEvent( 'user_password_update_failure', array( 'object_type' => $object_type, 'dn' => $dn, 'error' => ldap_errno($ldap) !== 0?ldap_error($ldap):'unknown' ) ); if (ldap_errno($ldap) !== 0) { LSerror :: addErrorCode('LSldap_10', ldap_error($ldap)); } else { LSerror :: addErrorCode('LSldap_11'); } return false; } return $changed_attrs; } /** * Return a configuration parameter (or default value) * * @param string $param The configuration parameter * @param mixed $default The default value (default : null) * @param string $cast Cast resulting value in specific type (default : disabled) * * @return mixed The configuration parameter value or default value if not set **/ private static function getConfig($param, $default=null, $cast=null) { return LSconfig :: get($param, $default, $cast, self :: $config); } /** * Registered an action on a specific event * * @param string $event The event name * @param callable $callable The callable to run on event * @param array $params Paremeters that will be pass to the callable * * @return void */ public static function addEvent($event, $callable, $params=NULL) { self :: $_events[$event][] = array( 'callable' => $callable, 'params' => is_array($params)?$params:array(), ); } /** * Run triggered actions on specific event * * @param string $event Event name * @param mixed $data Event data * * @return boolean True if all triggered actions succefully runned, false otherwise */ public static function fireEvent($event, $data=null) { $return = true; // Binding via addEvent if (isset(self :: $_events[$event]) && is_array(self :: $_events[$event])) { foreach (self :: $_events[$event] as $e) { if (is_callable($e['callable'])) { try { call_user_func_array( $e['callable'], array_merge( array($data), $e['params'] ) ); } catch(Exception $er) { LSerror :: addErrorCode( 'LSldap_13', array('callable' => format_callable($e['callable']), 'event' => $event) ); $return = false; } } else { LSerror :: addErrorCode( 'LSldap_12', array('callable' => format_callable($e['callable']), 'event' => $event) ); $return = false; } } } return $return; } } /* * Error Codes */ LSerror :: defineError('LSldap_01', ___("LSldap: Error during the LDAP server connection (%{msg}).") ); LSerror :: defineError('LSldap_02', ___("LSldap: Error during the LDAP search (%{msg}).") ); LSerror :: defineError('LSldap_03', ___("LSldap: Object type unknown.") ); LSerror :: defineError('LSldap_04', ___("LSldap: Error while fetching the LDAP entry.") ); LSerror :: defineError('LSldap_05', ___("LSldap: Error while changing the LDAP entry (DN : %{dn}).") ); LSerror :: defineError('LSldap_06', ___("LSldap: Error while deleting empty attributes.") ); LSerror :: defineError('LSldap_07', ___("LSldap: Error while changing the DN of the object.") ); LSerror :: defineError('LSldap_08', ___("LSldap: LDAP server base DN not configured.") ); LSerror :: defineError('LSldap_09', ___("LSldap: Fail to set authz proxy option on LDAP server connection.") ); LSerror :: defineError('LSldap_10', ___("LSldap: Error while changing the user password: %{msg}.") ); LSerror :: defineError('LSldap_11', ___("LSldap: Unknown LDAP error while updating user password") ); LSerror :: defineError('LSldap_12', ___("LSldap: Fail to execute trigger %{callable} on event %{event} : is not callable.") ); LSerror :: defineError('LSldap_13', ___("LSldap: Error during the execution of the trigger %{callable} on event %{event}.") );