diff --git a/lib/kolab_api_service.php b/lib/kolab_api_service.php index 1c8480f..670aaaf 100644 --- a/lib/kolab_api_service.php +++ b/lib/kolab_api_service.php @@ -1,725 +1,725 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | | Author: Jeroen van Meeuwen | +--------------------------------------------------------------------------+ */ /** * Interface class for Kolab Admin Services */ abstract class kolab_api_service { protected $base_dn = null; protected $cache = array(); protected $conf; protected $controller; protected $db; protected $supported_types_db = array('ou', 'group', 'resource', 'role', 'sharedfolder', 'user'); protected $supported_types = array('domain', 'ou', 'group', 'resource', 'role', 'sharedfolder', 'user'); /** * Class constructor. * * @param kolab_api_controller Controller */ public function __construct($ctrl) { $this->controller = $ctrl; $this->conf = Conf::get_instance(); $this->db = SQL::get_instance(); } /** * Advertise this service's capabilities */ abstract public function capabilities($domain); /** * Returns attributes of specified user type. * * @param string $object_name Name of the object (user, group, etc.) * @param int $type_id User type identifier * @param string $key_name Reference to a variable which will be set to type key * * @return array User type attributes */ protected function object_type_attributes($object_name, $type_id, &$key_name = null) { if (!$object_name || !in_array($object_name, $this->supported_types)) { return array(); } $object_types = $this->object_types($object_name); if (empty($type_id)) { if (count($object_types) == 1) { $type_id = key($object_types); } else { throw new Exception($this->controller->translate('api.notypeid'), 34); } } else if ($type_id && empty($object_types[$type_id])) { throw new Exception($this->controller->translate('api.invalidtypeid'), 35); } $key_name = $object_types[$type_id]['key']; return $object_types[$type_id]['attributes']; } /** * Detects object type ID for specified objectClass attribute value * * @param string $object_name Name of the object (user, group, etc.) * @param array $attributes Array of attributes and values * * @return int Object type identifier */ protected function object_type_id($object_name, $attributes) { if ($object_name == 'domain') return 1; $object_class = $attributes['objectclass']; if (empty($object_class)) { return null; } $object_types = $this->object_types($object_name); if (count($object_types) == 1) { return key($object_types); } $object_class = array_map('strtolower', $object_class); $object_keys = array_diff(array_keys($attributes), array(self::unique_attribute())); $keys_count = count($object_keys); $class_count = count($object_class); $type_score = null; $type_id = null; Log::trace("kolab_api_service::object_type_id objectClasses: " . implode(", ", $object_class)); foreach ($object_types as $idx => $elem) { $ref_class = $elem['attributes']['fields']['objectclass']; if (empty($ref_class)) { continue; } Log::trace("Reference objectclasses for " . $elem['key'] . ": " . implode(", ", $ref_class)); $elem_keys_score = 0; $elem_values_score = 0; $delta = 0; // Eliminate the duplicates between the $data_ocs and $ref_ocs $_object_class = array_diff($object_class, $ref_class); $_ref_class = array_diff($ref_class, $object_class); // Object classes score $differences = count($_object_class) + count($_ref_class); $commonalities = $class_count - $differences; $elem_score = $differences > 0 ? round($commonalities / $differences, 2) : $commonalities; // Attributes score if ($keys_count) { $ref_keys = array_unique(array_merge( array_keys((array) $elem['attributes']['auto_form_fields']), array_keys((array) $elem['attributes']['form_fields']), array_keys($elem['attributes']['fields']) )); $elem_keys_score = $keys_count - count(array_diff($object_keys, $ref_keys)); } // Static attributes score $elem_values_score = 0; foreach ((array) $elem['attributes']['fields'] as $attr => $value) { // Skip the object classes we have already compared if ($attr == "objectclass") { continue; } $v = $attributes[$attr]; if (is_array($value)) { foreach ($value as $_value) { $_value = $this->conf->expand($_value, $custom = Array('base_dn' => $this->base_dn())); if (in_array($_value, (array)$v)) { $elem_values_score++; } } $value = implode('', $value); } else { $value = $this->conf->expand($_value, $custom = Array('base_dn' => $this->base_dn())); } if (is_array($v)) { $v = implode('', $v); } $elem_values_score += intval($v == $value); } // Position in tree score if (!empty($elem['attributes']['fields']['ou'])) { if (!empty($attributes['ou'])) { if (strtolower($elem['attributes']['fields']['ou']) == strtolower($attributes['ou'])) { Log::trace("object_type " . $elem['key'] . " fields ou setting matches entry, bumping scores."); $elem_score += 2; $elem_keys_score += 10; } } } // On the likely chance that the object is a resource (types of which likely have the same // set of objectclass attribute values), consider the other attributes. (#853) if ($object_name == 'resource') { //console("From database", $elem); //console("Element key is " . $elem['key'] . " and \$attributes['mail'] is " . $attributes['mail']); if (strpos($attributes['mail'], 'resource-' . $elem['key'] . '-') === 0) { $elem_score += 10; } } // degrade class score if object contains more attributes // than defined in object type if ($keys_count && $elem_keys_score < $keys_count) { $delta -= $class_count - round(($keys_count / $elem_keys_score) * $class_count, 2); if ($delta > 0) { $elem_score -= $delta; } } $elem_score .= ':' . $elem_keys_score . ':' . $elem_values_score; // fix decimal separator for score_compare() and consistent log (#4799) $elem_score = str_replace(',', '.', $elem_score); $delta = str_replace(',', '.', $delta); Log::trace("Score for $object_name type " . $elem['name'] . ": $elem_score ($commonalities/$differences/$delta)"); // Compare last and current element (object type) score if ($this->score_compare($elem_score, $type_score)) { $type_id = $idx; $type_score = $elem_score; } } return $type_id; } /** * Returns object types definitions. * * @param string $object_name Name of the object (user, group, etc.) * * @return array Object types. */ protected function object_types($object_name) { if (!$object_name || !in_array($object_name, $this->supported_types)) { return array(); } $conf = Conf::get_instance(); $devel_mode = $conf->get('kolab_wap', 'devel_mode'); if ($devel_mode == null) { if (!empty($this->cache['object_types']) && !empty($this->cache['object_types'][$object_name])) { return $this->cache['object_types'][$object_name]; } } // get list of object types if ($object_name == 'domain') { $object_types = array( '1' => array( 'key' => 'default', 'attributes' => kolab_api_service_domain_types::$DEFAULT_TYPE_ATTRS, ), ); $object_types['1']['attributes']['form_fields']['aci'] = array( 'type' => 'list', 'optional' => true, ); } else { $sql_result = $this->db->query("SELECT * FROM `{$object_name}_types` ORDER BY `name`"); $object_types = array(); while ($row = $this->db->fetch_assoc($sql_result)) { $object_types[$row['id']] = array(); foreach ($row as $key => $value) { if ($key != "id") { if ($key == "attributes") { $object_types[$row['id']][$key] = json_decode($value, true); } else { $object_types[$row['id']][$key] = $value; } } } } } if ($devel_mode == null) { return $this->cache['object_types'][$object_name] = $object_types; } else { return $object_types; } } /** * Parses input (for add/edit) attributes * * @param string $object_name Name of the object (user, group, etc.) * @param array $attrs Entry attributes * * @return array Entry attributes */ protected function parse_input_attributes($object_name, $attribs) { $type_attrs = $this->object_type_attributes($object_name, $attribs['type_id']); Log::trace("kolab_api_service::parse_input_attributes for $object_name: " . var_export($type_attrs, TRUE)); Log::trace("called with \$attribs: " . var_export($attribs, TRUE)); $form_service = $this->controller->get_service('form_value'); // With the result, start validating the input $attribs['object_type'] = $object_name; $validate_result = $form_service->validate(null, $attribs); $special_attr_validate = Array(); foreach ($validate_result as $attr_name => $value) { if ($value !== false && $value !== '' && $value !== null && $value !== "OK") { $special_attr_validate[$attr_name] = $value; } } Log::trace("kolab_api_service::parse_input_attributes() \$special_attr_validate: " . var_export($special_attr_validate, TRUE)); $result = array(); if (isset($type_attrs['form_fields'])) { foreach ($type_attrs['form_fields'] as $key => $value) { Log::trace("Running parse input attributes for key $key"); $type = $value['type'] ?: ($type_attrs['auto_form_fields'][$key] ? $type_attrs['auto_form_fields'][$key]['type'] : ''); if (($type == 'text' || empty($type)) && is_array($attribs[$key])) { $attribs[$key] = array_shift($attribs[$key]); } if (empty($attribs[$key]) && empty($value['optional'])) { Log::error("\$attribs['" . $key . "'] is empty, and the field is not optional"); throw new Exception("Missing input value for $key", 345); } else { Log::trace("Either \$attribs['" . $key . "'] is not empty or the field is optional"); $result[$key] = $attribs[$key]; } } } if (isset($type_attrs['auto_form_fields'])) { foreach ($type_attrs['auto_form_fields'] as $key => $value) { if (empty($attribs[$key])) { if (empty($value['optional'])) { $attribs['attributes'] = array($key); $res = $form_service->generate(null, $attribs); $attribs[$key] = $res[$key]; $result[$key] = $attribs[$key]; } } else { $result[$key] = $attribs[$key]; } } } if (isset($type_attrs['fields'])) { foreach ($type_attrs['fields'] as $key => $value) { if (!is_array($value)) { $value2 = $this->conf->expand($value, $custom = Array('base_dn' => $this->base_dn())); if ($value !== $value2) { Log::trace("Made value " . var_export($value, TRUE) . " in to: " . var_export($value2, TRUE)); $value = $value2; } } else { foreach ($value as $_key => $_value) { $_value2 = $this->conf->expand($_value, $custom = Array('base_dn' => $this->base_dn())); if ($_value !== $_value2) { Log::trace("Made value " . var_export($_value, TRUE) . " in to: " . var_export($_value2, TRUE)); $value[$_key] = $_value2; } } } if (empty($attribs[$key])) { $result[$key] = $type_attrs['fields'][$key] = $value; } else { if (!empty($type_attrs['auto_form_fields'][$key]['optional']) && $type_attrs['auto_form_fields'][$key]['type'] == "list") { $result[$key] = array_unique(array_merge((array)$attribs[$key], (array)$value)); } else { $result[$key] = $attribs[$key] = $value; } } } } // OU's parent attribute if ($object_name == 'ou' && !empty($attribs['base_dn'])) { // @TODO: validate? $result['base_dn'] = $attribs['base_dn']; } $result = array_merge($result, $special_attr_validate); Log::trace("parse_input_attributes result (merge of \$result and \$special_attr_validate)", $result); return $result; } protected function parse_list_attributes($post) { $attributes = Array(); // Attributes to return if (!empty($post['attributes']) && is_array($post['attributes'])) { // get only supported attributes $attributes = array_intersect($this->list_attribs, $post['attributes']); // need to fix array keys $attributes = array_values($attributes); // unique attribute is always allowed if (($key = array_search('id', $post['attributes'])) !== false) { $attributes[] = self::unique_attribute(); } } if (empty($attributes)) { $attributes = (array)$this->list_attribs[0]; } return $attributes; } protected function parse_list_result($result) { if (!empty($result) && !empty($result['count'])) { $unique_attr = self::unique_attribute(); // replace back unique attribute name with 'id' foreach ($result['list'] as $idx => $record) { // if not set, we assume unique attribute wasn't requested if (!isset($record[$unique_attr])) { break; } $result['list'][$idx]['id'] = $record[$unique_attr]; unset($result['list'][$idx][$unique_attr]); } } return $result; } protected function parse_list_params($post) { $params = Array(); if (!empty($post['sort_by'])) { if (is_array($post['sort_by'])) { $params['sort_by'] = Array(); foreach ($post['sort_by'] as $attrib) { if (in_array($attrib, $this->list_attribs)) { $params['sort_by'][] = $attrib; } } } else { // check if sort attribute is supported if (in_array($post['sort_by'], $this->list_attribs)) { $params['sort_by'] = $post['sort_by']; } } } if (!empty($post['sort_order'])) { $params['sort_order'] = $post['sort_order'] == 'DESC' ? 'DESC' : 'ASC'; } if (!empty($post['page'])) { $params['page'] = $post['page']; } if (!empty($post['page_size'])) { $params['page_size'] = $post['page_size']; } return $params; } protected function parse_list_search($post) { $search = Array(); // Search parameters if (!empty($post['search']) && is_array($post['search'])) { if (array_key_exists('params', $post['search'])) { $search = $post['search']; } else { $search['params'] = $post['search']; } if (!empty($post['search_operator'])) { $search['operator'] = $post['search_operator']; } } return $search; } /** * Parses result attributes * * @param string $object_name Name of the object (user, group, etc.) * @param array $attrs Entry attributes * * @return array Entry attributes */ public function parse_result_attributes($object_name, $attrs = array()) { if (empty($attrs) || !is_array($attrs)) { return $attrs; } $dn = key($attrs); $attrs = $attrs[$dn]; $extra_attrs = array(); $type_id = $this->object_type_id($object_name, $attrs); $unique_attr = self::unique_attribute(); // Search for attributes associated with the type_id that are not part // of the result returned earlier. Example: nsrole / nsroledn / aci, etc. if ($type_id) { $uta = $this->object_type_attributes($object_name, $type_id); $attributes = array_merge( array_keys((array) $uta['auto_form_fields']), array_keys((array) $uta['form_fields']), array_keys((array) $uta['fields']) ); $attributes = array_filter($attributes); $attributes = array_unique($attributes); $object_attributes = array_keys($attrs); // extra attributes $extra_attrs = array_diff($attributes, $object_attributes); // remove attributes not listed in object type definition // @TODO: make this optional? $attributes = array_flip(array_merge($attributes, array($unique_attr))); $attrs = array_intersect_key($attrs, $attributes); } /* $auth = Auth::get_instance(); // Insert the persistent, unique attribute if (!array_key_exists($unique_attr, $attrs)) { $extra_attrs[] = $unique_attr; } // Get extra attributes if (!empty($extra_attrs)) { $extra_attrs = $auth->get_entry_attributes($dn, array_values($extra_attrs)); if (!empty($extra_attrs)) { $attrs = array_merge($attrs, $extra_attrs); } } */ // Replace unique attribute with 'id' key $attrs['id'] = $attrs[$unique_attr]; unset($attrs[$unique_attr]); // add object type id to the result $attrs['type_id'] = $type_id; // always return entrydn $attrs['entrydn'] = $dn; // add organizational unit to the result if (empty($attrs['ou']) && isset($attributes['ou'])) { $dn = kolab_utils::explode_dn($dn); // pop the rdn unset($dn[0]); $attrs['ou'] = implode(',', $dn); } return $attrs; } /** * Returns all supported attributes of specified object type * * @param string $object_name Name of the object (user, group, etc.) * * @return array Entry attributes */ public function object_attributes($object_name) { $unique_attr = self::unique_attribute(); $object_types = $this->object_types($object_name); $attributes = array(); // because we don't know the object type identifier before // we get it from LDAP we need to get try attributes of all types foreach ($object_types as $type) { $attributes = array_merge( $attributes, array_keys((array) $type['attributes']['auto_form_fields']), array_keys((array) $type['attributes']['form_fields']), array_keys((array) $type['attributes']['fields']) ); } // use array_values, because ldap_read() does not like an array // with removed elements (holes in the index) $attributes = array_values(array_unique($attributes)); if (empty($attributes)) { $attributes = array('*'); } // Insert the persistent, unique attribute if (!array_key_exists($unique_attr, $attributes)) { $attributes[] = $unique_attr; } return $attributes; } /** * Compare two score values * * @param string $s1 Score * @param string $s2 Score * * @return bool True when $s1 is greater than $s2 */ protected function score_compare($s1, $s2) { if (empty($s2) && !empty($s1)) { return true; } $s1 = explode(':', $s1); $s2 = explode(':', $s2); foreach ($s1 as $key => $val) { if ($val > $s2[$key]) { return true; } if ($val < $s2[$key]) { return false; } } return false; } /** * Returns name of unique attribute * * @return string Unique attribute name */ public static function unique_attribute() { $conf = Conf::get_instance(); $unique_attr = $conf->get('unique_attribute'); if (!$unique_attr) { $unique_attr = 'nsuniqueid'; } return $unique_attr; } /** * Returns unique attribute for specified entry DN * * @return string Unique attribute value */ protected function unique_attribute_value($dn) { // this method can be called internally quite often // let's cache results in memory if (!empty($this->cache['unique_attributes'][$dn])) { return $this->cache['unique_attributes'][$dn]; } $unique_attr = self::unique_attribute(); $auth = Auth::get_instance(); $result = $auth->get_entry_attribute($dn, $unique_attr); return $this->cache['unique_attributes'][$dn] = $result; } private function base_dn() { if (!empty($this->base_dn)) { return $this->base_dn; } // Get the domain information for expansion later $auth = Auth::get_instance(); $domain_info = $auth->domain_info($_SESSION['user']->get_domain()); $domain_info = $domain_info[key($domain_info)]; $dna = $this->conf->get('domain_name_attribute'); if (empty($dna)) { $dna = 'associateddomain'; } $domain = $domain_info[$dna]; if (is_array($domain)) { $domain = $domain[0]; } $dba = 'inetdomainbasedn'; if (empty($domain_info[$dba])) { - $this->base_dn = 'dc=' . implode('dc=,', explode('.', $domain)); + $this->base_dn = 'dc=' . implode(',dc=', explode('.', $domain)); } else { $this->base_dn = $domain_info[$dba]; } return $this->base_dn; } }