/** * Set the hook as failed w/ the given message * * @param string $message Error message or error format string * @param mixed ...$arg Format string argument */ private function fail($message) { $args = array_slice(func_get_args(), 1); $lastError = vsprintf($message, $args); Logger::debug($lastError); $this->lastError = $lastError; }
/** * Set the filter and render it internally. * * @param Filter $filter * * @return $this * * @throws ProgrammingError */ public function setFilter(Filter $filter) { $this->filter = $filter; $this->query = $this->renderFilter($this->filter); Logger::debug('Rendered elasticsearch filter: %s', json_encode($this->query)); return $this; }
/** * Append the given log entry or nested inspection * * @throws ProgrammingError When called after erroring * * @param $entry string|Inspection A log entry or nested inspection */ public function write($entry) { if (isset($this->error)) { throw new ProgrammingError('Inspection object used after error'); } if ($entry instanceof Inspection) { $this->log[$entry->description] = $entry->toArray(); } else { Logger::debug($entry); $this->log[] = $entry; } }
/** * Resolve a macro based on the given object * * @param string $macro The macro to resolve * @param MonitoredObject|stdClass $object The object used to resolve the macro * * @return string The new value or the macro if it cannot be resolved */ public static function resolveMacro($macro, $object) { if (isset(self::$icingaMacros[$macro]) && isset($object->{self::$icingaMacros[$macro]})) { return $object->{self::$icingaMacros[$macro]}; } try { $value = $object->{$macro}; } catch (Exception $e) { $value = null; Logger::debug('Unable to resolve macro "%s". An error occured: %s', $macro, $e); } return $value !== null ? $value : $macro; }
/** * Create and return a new navigation item for the given configuration * * @param string $name * @param array|ConfigObject $properties * * @return NavigationItem * * @throws InvalidArgumentException If the $properties argument is neither an array nor a ConfigObject */ public function createItem($name, $properties) { if ($properties instanceof ConfigObject) { $properties = $properties->toArray(); } elseif (!is_array($properties)) { throw new InvalidArgumentException('Argument $properties must be of type array or ConfigObject'); } $itemType = isset($properties['type']) ? String::cname($properties['type'], '-') : 'NavigationItem'; if (!empty(static::$types) && isset(static::$types[$itemType])) { return new static::$types[$itemType]($name, $properties); } $item = null; foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { $classPath = 'Icinga\\Module\\' . ucfirst($module->getName()) . '\\' . static::NAVIGATION_NS . '\\' . $itemType; if (class_exists($classPath)) { $item = new $classPath($name, $properties); break; } } if ($item === null) { $classPath = 'Icinga\\' . static::NAVIGATION_NS . '\\' . $itemType; if (class_exists($classPath)) { $item = new $classPath($name, $properties); } } if ($item === null) { Logger::debug('Failed to find custom navigation item class %s for item %s. Using base class NavigationItem now', $itemType, $name); $item = new NavigationItem($name, $properties); static::$types[$itemType] = 'Icinga\\Web\\Navigation\\NavigationItem'; } elseif (!$item instanceof NavigationItem) { throw new ProgrammingError('Class %s must inherit from NavigationItem', $classPath); } else { static::$types[$itemType] = $classPath; } return $item; }
protected function sendCommand(IcingaApiCommand $command) { Logger::debug('Sending Icinga command "%s" to the API "%s:%u"', $command->getEndpoint(), $this->getHost(), $this->getPort()); $response = RestRequest::post($this->getUriFor($command->getEndpoint()))->authenticateWith($this->getUsername(), $this->getPassword())->sendJson()->noStrictSsl()->setPayload($command->getData())->send(); if (isset($response['error'])) { throw new CommandTransportException('Can\'t send external Icinga command: %u %s', $response['error'], $response['status']); } $result = array_pop($response['results']); if ($result['code'] < 200 || $result['code'] >= 300) { throw new CommandTransportException('Can\'t send external Icinga command: %u %s', $result['code'], $result['status']); } if ($command->hasNext()) { $this->sendCommand($command->getNext()); } }
/** * Write the command to the Icinga command file on the remote host * * @param IcingaCommand $command * @param int|null $now * * @throws ConfigurationError * @throws TransportException */ public function send(IcingaCommand $command, $now = null) { if (!isset($this->path)) { throw new ConfigurationError('Can\'t send external Icinga Command. Path to the remote command file is missing'); } if (!isset($this->host)) { throw new ConfigurationError('Can\'t send external Icinga Command. Remote host is missing'); } $commandString = $this->renderer->render($command, $now); Logger::debug('Sending external Icinga command "%s" to the remote command file "%s:%u%s"', $commandString, $this->host, $this->port, $this->path); $ssh = sprintf('ssh -o BatchMode=yes -p %u', $this->port); // -o BatchMode=yes for disabling interactive authentication methods if (isset($this->user)) { $ssh .= sprintf(' -l %s', escapeshellarg($this->user)); } if (isset($this->privateKey)) { $ssh .= sprintf(' -o StrictHostKeyChecking=no -i %s', escapeshellarg($this->privateKey)); } $ssh .= sprintf(' %s "echo %s > %s" 2>&1', escapeshellarg($this->host), escapeshellarg($commandString), escapeshellarg($this->path)); exec($ssh, $output, $status); if ($status !== 0) { throw new TransportException('Can\'t send external Icinga command: %s', implode(' ', $output)); } }
/** * Add a sort rule for this query * * If called without a specific column, the repository's defaul sort rules will be applied. * This notifies the repository about each column being required as filter column. * * @param string $field The name of the column by which to sort the query's result * @param string $direction The direction to use when sorting (asc or desc, default is asc) * @param bool $ignoreDefault Whether to ignore any default sort rules if $field is given * * @return $this */ public function order($field = null, $direction = null, $ignoreDefault = false) { $sortRules = $this->getSortRules(); if ($field === null) { // Use first available sort rule as default if (empty($sortRules)) { // Return early in case of no sort defaults and no given $field return $this; } $sortColumns = reset($sortRules); if (!array_key_exists('columns', $sortColumns)) { $sortColumns['columns'] = array(key($sortRules)); } if ($direction !== null || !array_key_exists('order', $sortColumns)) { $sortColumns['order'] = $direction ?: static::SORT_ASC; } } else { $alias = $this->repository->reassembleQueryColumnAlias($this->target, $field) ?: $field; if (!$ignoreDefault && array_key_exists($alias, $sortRules)) { $sortColumns = $sortRules[$alias]; if (!array_key_exists('columns', $sortColumns)) { $sortColumns['columns'] = array($alias); } if ($direction !== null || !array_key_exists('order', $sortColumns)) { $sortColumns['order'] = $direction ?: static::SORT_ASC; } } else { $sortColumns = array('columns' => array($alias), 'order' => $direction); } } $baseDirection = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC; foreach ($sortColumns['columns'] as $column) { list($column, $specificDirection) = $this->splitOrder($column); if ($this->hasLimit() && $this->repository->providesValueConversion($this->target, $column)) { Logger::debug('Cannot order by column "%s" in repository "%s". The query is' . ' limited and applies value conversion rules on the column', $column, $this->repository->getName()); continue; } try { $this->query->order($this->repository->requireFilterColumn($this->target, $column, $this), $specificDirection ?: $baseDirection); } catch (QueryException $_) { Logger::info('Cannot order by column "%s" in repository "%s"', $column, $this->repository->getName()); } } return $this; }
/** * Create or return an instance of a given hook * * TODO: Should return some kind of a hook interface * * @param string $name One of the predefined hook names * @param string $key The identifier of a specific subtype * * @return mixed */ public static function createInstance($name, $key) { $name = self::normalizeHookName($name); if (!self::has($name, $key)) { return null; } if (isset(self::$instances[$name][$key])) { return self::$instances[$name][$key]; } $class = self::$hooks[$name][$key]; if (!class_exists($class)) { throw new ProgrammingError('Erraneous hook implementation, class "%s" does not exist', $class); } try { $instance = new $class(); } catch (Exception $e) { Logger::debug('Hook "%s" (%s) (%s) failed, will be unloaded: %s', $name, $key, $class, $e->getMessage()); // TODO: Persist unloading for "some time" or "current session" unset(self::$hooks[$name][$key]); return null; } self::assertValidHook($instance, $name); self::$instances[$name][$key] = $instance; return $instance; }
/** * Detect installed modules from every path provided in modulePaths * * @param array $availableDirs Installed modules location * * @return $this */ public function detectInstalledModules(array $availableDirs = null) { $modulePaths = $availableDirs !== null ? $availableDirs : $this->modulePaths; foreach ($modulePaths as $basedir) { $canonical = realpath($basedir); if ($canonical === false) { Logger::warning('Module path "%s" does not exist', $basedir); continue; } if (!is_dir($canonical)) { Logger::error('Module path "%s" is not a directory', $canonical); continue; } if (!is_readable($canonical)) { Logger::error('Module path "%s" is not readable', $canonical); continue; } if (($dh = opendir($canonical)) !== false) { while (($file = readdir($dh)) !== false) { if ($file[0] === '.') { continue; } if (is_dir($canonical . '/' . $file)) { if (!array_key_exists($file, $this->installedBaseDirs)) { $this->installedBaseDirs[$file] = $canonical . '/' . $file; } else { Logger::debug('Module "%s" already exists in installation path "%s" and is ignored.', $canonical . '/' . $file, $this->installedBaseDirs[$file]); } } } closedir($dh); } } ksort($this->installedBaseDirs); return $this; }
/** * Prepare and establish a connection with the LDAP server * * @return resource A LDAP link identifier * * @throws LdapException In case the connection is not possible */ protected function prepareNewConnection() { if ($this->encryption === static::STARTTLS || $this->encryption === static::LDAPS) { $this->prepareTlsEnvironment(); } $hostname = $this->hostname; if ($this->encryption === static::LDAPS) { $hostname = 'ldaps://' . $hostname; } $ds = ldap_connect($hostname, $this->port); // Usage of ldap_rename, setting LDAP_OPT_REFERRALS to 0 or using STARTTLS requires LDAPv3. // If this does not work we're probably not in a PHP 5.3+ environment as it is VERY // unlikely that the server complains about it by itself prior to a bind request ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); // Not setting this results in "Operations error" on AD when using the whole domain as search base ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); if ($this->encryption === static::STARTTLS) { if ($this->encryptionSuccess = @ldap_start_tls($ds)) { Logger::debug('LDAP STARTTLS succeeded'); } else { Logger::error('LDAP STARTTLS failed: %s', ldap_error($ds)); // ldap_start_tls seems to corrupt the connection though if I understand // https://tools.ietf.org/html/rfc4511#section-4.14.2 correctly, this shouldn't happen $ds = ldap_connect($hostname, $this->port); ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); } } elseif ($this->encryption === static::LDAPS) { $this->encryptionSuccess = true; } return $ds; }
/** * Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation * * @param string|null $value * * @return int */ protected function retrieveGeneralizedTime($value) { if ($value === null) { return $value; } if (($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false || ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false || ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false || ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false || ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false || ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false) { return $dateTime->getTimeStamp(); } else { Logger::debug(sprintf('Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".', $value, $this->getName())); } }
/** * Perform a LDAP search and return the result * * @param LdapQuery $query * @param array $attributes An array of the required attributes * @param int $attrsonly Should be set to 1 if only attribute types are wanted * @param int $sizelimit Enables you to limit the count of entries fetched * @param int $timelimit Sets the number of seconds how long is spend on the search * @param int $deref * * @return resource|bool A search result identifier or false on error * * @throws LogicException If the LDAP query search scope is unsupported */ public function ldapSearch(LdapQuery $query, array $attributes = null, $attrsonly = 0, $sizelimit = 0, $timelimit = 0, $deref = LDAP_DEREF_NEVER) { $queryString = (string) $query; $baseDn = $query->getBase() ?: $this->getDn(); $scope = $query->getScope(); if (Logger::getInstance()->getLevel() === Logger::DEBUG) { // We're checking the level by ourself to avoid rendering the ldapsearch commandline for nothing $starttlsParam = $this->encryption === static::STARTTLS ? ' -ZZ' : ''; $ldapUrl = ($this->encryption === static::LDAPS ? 'ldaps://' : 'ldap://') . $this->hostname . ($this->port ? ':' . $this->port : ''); if ($this->bound) { $bindParams = ' -D "' . $this->bindDn . '"' . ($this->bindPw ? ' -W' : ''); } if ($deref === LDAP_DEREF_NEVER) { $derefName = 'never'; } elseif ($deref === LDAP_DEREF_ALWAYS) { $derefName = 'always'; } elseif ($deref === LDAP_DEREF_SEARCHING) { $derefName = 'search'; } else { // $deref === LDAP_DEREF_FINDING $derefName = 'find'; } Logger::debug("Issueing LDAP search. Use '%s' to reproduce.", sprintf('ldapsearch -P 3%s -H "%s"%s -b "%s" -s "%s" -z %u -l %u -a "%s"%s%s%s', $starttlsParam, $ldapUrl, $bindParams, $baseDn, $scope, $sizelimit, $timelimit, $derefName, $attrsonly ? ' -A' : '', $queryString ? ' "' . $queryString . '"' : '', $attributes ? ' "' . join('" "', $attributes) . '"' : '')); } switch ($scope) { case LdapQuery::SCOPE_SUB: $function = 'ldap_search'; break; case LdapQuery::SCOPE_ONE: $function = 'ldap_list'; break; case LdapQuery::SCOPE_BASE: $function = 'ldap_read'; break; default: throw new LogicException('LDAP scope %s not supported by ldapSearch', $scope); } return @$function($this->getConnection(), $baseDn, $queryString, $attributes, $attrsonly, $sizelimit, $timelimit, $deref); }
/** * Return the distinguished name for the given uid or gid * * @param string $name * * @return string */ protected function persistUserName($name) { try { $userDn = $this->ds->select()->from($this->userClass, array())->where($this->userNameAttribute, $name)->setBase($this->userBaseDn)->setUsePagedResults(false)->fetchDn(); if ($userDn) { return $userDn; } $groupDn = $this->ds->select()->from($this->groupClass, array())->where($this->groupNameAttribute, $name)->setBase($this->groupBaseDn)->setUsePagedResults(false)->fetchDn(); if ($groupDn) { return $groupDn; } } catch (LdapException $_) { // pass } Logger::debug('Unable to persist uid or gid "%s" in repository "%s". No DN found.', $name, $this->getName()); return $name; }
/** * @return string */ public function __toString() { try { $select = (string) $this->getSelectQuery(); return $this->getIsSubQuery() ? '(' . $select . ')' : $select; } catch (Exception $e) { Logger::debug('Failed to render DbQuery. An error occured: %s', $e); return ''; } }
/** * Send the request * * @return mixed * * @throws Exception */ public function send() { $defaults = array('host' => 'localhost', 'path' => '/'); $url = array_merge($defaults, parse_url($this->uri)); if (isset($url['port'])) { $url['host'] .= sprintf(':%u', $url['port']); } if (isset($url['query'])) { $url['path'] .= sprintf('?%s', $url['query']); } $headers = array("{$this->method} {$url['path']} HTTP/1.1", "Host: {$url['host']}", "Content-Type: {$this->contentType}", 'Accept: application/json', 'Expect:'); $ch = curl_init(); $options = array(CURLOPT_URL => $this->uri, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_PROXY => '', CURLOPT_CUSTOMREQUEST => $this->method, CURLOPT_RETURNTRANSFER => true); // Record cURL command line for debugging $curlCmd = array('curl', '-s', '-X', $this->method, '-H', escapeshellarg('Accept: application/json')); if ($this->strictSsl) { $options[CURLOPT_SSL_VERIFYHOST] = 2; $options[CURLOPT_SSL_VERIFYPEER] = true; } else { $options[CURLOPT_SSL_VERIFYHOST] = false; $options[CURLOPT_SSL_VERIFYPEER] = false; $curlCmd[] = '-k'; } if ($this->hasBasicAuth) { $options[CURLOPT_USERPWD] = sprintf('%s:%s', $this->username, $this->password); $curlCmd[] = sprintf('-u %s:%s', escapeshellarg($this->username), escapeshellarg($this->password)); } if (!empty($this->payload)) { $payload = $this->serializePayload($this->payload, $this->contentType); $options[CURLOPT_POSTFIELDS] = $payload; $curlCmd[] = sprintf('-d %s', escapeshellarg($payload)); } $options[CURLOPT_HTTPHEADER] = $headers; $stream = null; if (Logger::getInstance()->getLevel() === Logger::DEBUG) { $stream = fopen('php://temp', 'w'); $options[CURLOPT_VERBOSE] = true; $options[CURLOPT_STDERR] = $stream; } curl_setopt_array($ch, $options); Logger::debug('Executing %s %s', implode(' ', $curlCmd), escapeshellarg($this->uri)); $result = curl_exec($ch); if ($result === false) { throw new Exception(curl_error($ch)); } curl_close($ch); if (is_resource($stream)) { rewind($stream); Logger::debug(stream_get_contents($stream)); fclose($stream); } $response = @json_decode($result, true); if ($response === null) { if (version_compare(PHP_VERSION, '5.5.0', '>=')) { throw new Exception(json_last_error_msg()); } else { switch (json_last_error()) { case JSON_ERROR_DEPTH: $msg = 'The maximum stack depth has been exceeded'; break; case JSON_ERROR_CTRL_CHAR: $msg = 'Control character error, possibly incorrectly encoded'; break; case JSON_ERROR_STATE_MISMATCH: $msg = 'Invalid or malformed JSON'; break; case JSON_ERROR_SYNTAX: $msg = 'Syntax error'; break; case JSON_ERROR_UTF8: $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; break; default: $msg = 'An error occured when parsing a JSON string'; } throw new Exception($msg); } } return $response; }
/** * Remove session cookies */ protected function clearCookies() { if (ini_get('session.use_cookies')) { Logger::debug('Clear session cookie'); $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']); } }
/** * Write the command to the Icinga command file on the remote host * * @param IcingaCommand $command * @param int|null $now * * @throws ConfigurationError * @throws CommandTransportException */ public function send(IcingaCommand $command, $now = null) { if (!isset($this->path)) { throw new ConfigurationError('Can\'t send external Icinga Command. Path to the remote command file is missing'); } if (!isset($this->host)) { throw new ConfigurationError('Can\'t send external Icinga Command. Remote host is missing'); } $commandString = $this->renderer->render($command, $now); Logger::debug('Sending external Icinga command "%s" to the remote command file "%s:%u%s"', $commandString, $this->host, $this->port, $this->path); return $this->sendCommandString($commandString); }
/** * Return the form for the given type of navigation item * * @param string $type * * @return Form */ protected function getItemForm($type) { $className = StringHelper::cname($type, '-') . 'Form'; $form = null; foreach (Icinga::app()->getModuleManager()->getLoadedModules() as $module) { $classPath = 'Icinga\\Module\\' . ucfirst($module->getName()) . '\\' . static::FORM_NS . '\\' . $className; if (class_exists($classPath)) { $form = new $classPath(); break; } } if ($form === null) { $classPath = 'Icinga\\' . static::FORM_NS . '\\' . $className; if (class_exists($classPath)) { $form = new $classPath(); } } if ($form === null) { Logger::debug('Failed to find custom navigation item form %s for item %s. Using form NavigationItemForm now', $className, $type); $form = new NavigationItemForm(); } elseif (!$form instanceof NavigationItemForm) { throw new ProgrammingError('Class %s must inherit from NavigationItemForm', $classPath); } return $form; }
/** * Return the groups the given user is a member of * * @param User $user * * @return array */ public function getMemberships(User $user) { if ($this->groupClass === 'posixGroup') { // Posix group only uses simple user name $userDn = $user->getUsername(); } else { // LDAP groups use the complete DN if (($userDn = $user->getAdditional('ldap_dn')) === null) { $userQuery = $this->ds->select()->from($this->userClass)->where($this->userNameAttribute, $user->getUsername())->setBase($this->userBaseDn)->setUsePagedResults(false); if ($this->userFilter) { $userQuery->where(new Expression($this->userFilter)); } if (($userDn = $userQuery->fetchDn()) === null) { return array(); } } } $groupQuery = $this->ds->select()->from($this->groupClass, array($this->groupNameAttribute))->where($this->groupMemberAttribute, $userDn)->setBase($this->groupBaseDn); if ($this->groupFilter) { $groupQuery->where(new Expression($this->groupFilter)); } Logger::debug('Fetching groups for user %s using filter %s.', $user->getUsername(), $groupQuery->__toString()); $groups = array(); foreach ($groupQuery as $row) { $groups[] = $row->{$this->groupNameAttribute}; } Logger::debug('Fetched %d groups: %s.', count($groups), join(', ', $groups)); return $groups; }
/** * Create or return an instance of a given hook * * TODO: Should return some kind of a hook interface * * @param string $name One of the predefined hook names * @param string $key The identifier of a specific subtype * * @return mixed */ public static function createInstance($name, $key) { if (!self::has($name, $key)) { return null; } if (isset(self::$instances[$name][$key])) { return self::$instances[$name][$key]; } $class = self::$hooks[$name][$key]; try { $instance = new $class(); } catch (Exception $e) { Logger::debug('Hook "%s" (%s) (%s) failed, will be unloaded: %s', $name, $key, $class, $e->getMessage()); // TODO: Persist unloading for "some time" or "current session" unset(self::$hooks[$name][$key]); return null; } self::assertValidHook($instance, $name); self::$instances[$name][$key] = $instance; return $instance; }
/** * Write the command to the local Icinga command file * * @param IcingaCommand $command * @param int|null $now * * @throws ConfigurationError * @throws CommandTransportException */ public function send(IcingaCommand $command, $now = null) { if (!isset($this->path)) { throw new ConfigurationError('Can\'t send external Icinga Command. Path to the local command file is missing'); } $commandString = $this->renderer->render($command, $now); Logger::debug('Sending external Icinga command "%s" to the local command file "%s"', $commandString, $this->path); try { $file = new File($this->path, $this->openMode); $file->fwrite($commandString . "\n"); } catch (Exception $e) { $message = $e->getMessage(); if ($e instanceof RuntimeException && ($pos = strrpos($message, ':')) !== false) { // Assume RuntimeException thrown by SplFileObject in the format: __METHOD__ . "({$filename}): Message" $message = substr($message, $pos + 1); } throw new CommandTransportException('Can\'t send external Icinga command to the local command file "%s": %s', $this->path, $message); } }
/** * Clean up the given attributes and return them as simple object * * Applies column aliases, aggregates multi-value attributes as array and sets null for each missing attribute. * * @param array $attributes * @param array $requestedFields * * @return object */ public function cleanupAttributes($attributes, array $requestedFields) { // In case the result contains attributes with a differing case than the requested fields, it is // necessary to create another array to map attributes case insensitively to their requested counterparts. // This does also apply the virtual alias handling. (Since an LDAP server does not handle such) $loweredFieldMap = array(); foreach ($requestedFields as $name => $alias) { $loweredFieldMap[strtolower($name)] = is_string($alias) ? $alias : $name; } $cleanedAttributes = array(); for ($i = 0; $i < $attributes['count']; $i++) { $attribute_name = $attributes[$i]; if ($attributes[$attribute_name]['count'] === 1) { $attribute_value = $attributes[$attribute_name][0]; } else { $attribute_value = array(); for ($j = 0; $j < $attributes[$attribute_name]['count']; $j++) { $attribute_value[] = $attributes[$attribute_name][$j]; } } $requestedAttributeName = isset($loweredFieldMap[strtolower($attribute_name)]) ? $loweredFieldMap[strtolower($attribute_name)] : $attribute_name; $cleanedAttributes[$requestedAttributeName] = $attribute_value; } // The result may not contain all requested fields, so populate the cleaned // result with the missing fields and their value being set to null foreach ($requestedFields as $name => $alias) { if (!is_string($alias)) { $alias = $name; } if (!array_key_exists($alias, $cleanedAttributes)) { $cleanedAttributes[$alias] = null; Logger::debug('LDAP query result does not provide the requested field "%s"', $name); } } return (object) $cleanedAttributes; }
/** * Extract and return one or more objects from the given data * * @param array $objectPath * @param array $fields * @param array $data * * @param object|array */ protected function extractObject(array $objectPath, array $fields, array $data) { if (!$this->isAssociative($data)) { $values = array(); foreach ($data as $value) { if (is_array($value)) { $objectValue = $this->extractObject($objectPath, $fields, $value); if (is_array($objectValue)) { $values = array_merge($values, $objectValue); } elseif ($objectValue !== null) { $values[] = $objectValue; } } else { Logger::debug('Expected non-scalar value but got "%s" instead', $value); } } return $values; } $object = array_shift($objectPath); if (isset($data[$object])) { if (!is_array($data[$object])) { Logger::debug('Expected non-scalar value but got "%s" instead', $data[$object]); } elseif (!empty($objectPath)) { return $this->extractObject($objectPath, $fields, $data[$object]); } elseif ($this->isAssociative($data[$object])) { $properties = array(); foreach ($fields as $alias => $field) { if (isset($field['object_path'])) { $properties[$alias] = $this->extractObject($field['object_path'], $field['fields'], $data[$object]); } else { $properties[$alias] = $this->extractScalar($field, $data[$object]); } } return (object) $properties; } else { $objects = array(); foreach ($data[$object] as $objectData) { $properties = array(); foreach ($fields as $alias => $field) { if (isset($field['object_path'])) { $properties[$alias] = $this->extractObject($field['object_path'], $field['fields'], $objectData); } else { $properties[$alias] = $this->extractScalar($field, $objectData); } } $objects[] = (object) $properties; } return $objects; } } }
/** * Prepare and establish a connection with the LDAP server * * @return resource A positive LDAP link identifier * * @throws LdapException In case the connection is not possible */ protected function prepareNewConnection() { if ($this->encryption === static::STARTTLS || $this->encryption === static::LDAPS) { $this->prepareTlsEnvironment(); } $hostname = $this->hostname; if ($this->encryption === static::LDAPS) { $hostname = 'ldaps://' . $hostname; } $ds = ldap_connect($hostname, $this->port); try { $this->capabilities = $this->discoverCapabilities($ds); $this->discoverySuccess = true; } catch (LdapException $e) { Logger::debug($e); Logger::warning('LADP discovery failed, assuming default LDAP capabilities.'); $this->capabilities = new Capability(); // create empty default capabilities $this->discoverySuccess = false; } if ($this->encryption === static::STARTTLS) { $force_tls = false; if ($this->capabilities->hasStartTls()) { if (@ldap_start_tls($ds)) { Logger::debug('LDAP STARTTLS succeeded'); } else { Logger::error('LDAP STARTTLS failed: %s', ldap_error($ds)); throw new LdapException('LDAP STARTTLS failed: %s', ldap_error($ds)); } } elseif ($force_tls) { throw new LdapException('STARTTLS is required but not announced by %s', $this->hostname); } else { Logger::warning('LDAP STARTTLS enabled but not announced'); } } // ldap_rename requires LDAPv3: if ($this->capabilities->hasLdapV3()) { if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) { throw new LdapException('LDAPv3 is required'); } } else { // TODO: remove this -> FORCING v3 for now ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); Logger::warning('No LDAPv3 support detected'); } // Not setting this results in "Operations error" on AD when using the whole domain as search base ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); // ldap_set_option($ds, LDAP_OPT_DEREF, LDAP_DEREF_NEVER); return $ds; }