Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117754638
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
118 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/kolab_client_task.php b/lib/kolab_client_task.php
index 07864ff..2aedb27 100644
--- a/lib/kolab_client_task.php
+++ b/lib/kolab_client_task.php
@@ -1,1596 +1,1600 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab Web Admin Panel |
| |
| Copyright (C) 2011-2012, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 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 Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
| Author: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class kolab_client_task
{
/**
* @var kolab_client_output
*/
protected $output;
/**
* @var kolab_client_api
*/
protected $api;
/**
* @var Conf
*/
protected $config;
protected $ajax_only = false;
protected $page_title = 'Kolab Web Admin Panel';
protected $menu = array();
protected $cache = array();
protected $devel_mode = false;
protected $object_types = array('user', 'group', 'role', 'resource', 'sharedfolder', 'domain');
protected $page_size = 20;
protected $list_attribs = array();
protected $list_module;
protected $search_attribs = array(
'name' => array('cn', 'displayname'),
'email' => array('mail', 'alias', 'mailalternateaddress'),
'uid' => array('uid'),
);
protected static $translation = array();
/**
* Class constructor.
*
* @param kolab_client_output $output Optional output object
*/
public function __construct($output = null)
{
$this->config_init();
$this->devel_mode = $this->config_get('devel_mode', false, Conf::BOOL);
$this->output_init($output);
$this->api_init();
ini_set('session.use_cookies', 'On');
session_start();
$this->auth();
}
/**
* Localization initialization.
*/
protected function locale_init()
{
if (!empty(self::$translation)) {
return;
}
$language = $this->get_language();
$LANG = array();
if (!$language) {
$language = 'en_US';
}
@include INSTALL_PATH . '/locale/en_US.php';
if ($language != 'en_US') {
@include INSTALL_PATH . "/locale/$language.php";
}
setlocale(LC_ALL, $language . '.utf8', 'en_US.utf8');
self::$translation = $LANG;
}
/**
* Configuration initialization.
*/
private function config_init()
{
$this->config = Conf::get_instance();
}
/**
* Output initialization.
*/
private function output_init($output = null)
{
if ($output) {
$this->output = $output;
return;
}
$skin = $this->config_get('skin', 'default');
$this->output = new kolab_client_output($skin);
// Assign self to template variable
$this->output->assign('engine', $this);
}
/**
* API initialization
*/
private function api_init()
{
$url = $this->config_get('api_url', '');
if (!$url) {
$port = $_SERVER['SERVER_PORT'];
$url = kolab_utils::https_check() ? 'https://' : 'http://';
$url .= $_SERVER['SERVER_NAME'] . ($port ? ":$port" : '');
$url .= preg_replace('/\/?\?.*$/', '', $_SERVER['REQUEST_URI']);
$url .= '/api';
}
// TODO: Debug logging
//console($url);
$this->api = new kolab_client_api($url);
}
/**
* Returns system language (locale) setting.
*
* @return string Language code
*/
private function get_language()
{
$aliases = array(
'de' => 'de_DE',
'en' => 'en_US',
'pl' => 'pl_PL',
);
// UI language
$langs = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
$langs = explode(',', $langs);
if (!empty($_SESSION['user']) && !empty($_SESSION['user']['language'])) {
array_unshift($langs, $_SESSION['user']['language']);
}
while ($lang = array_shift($langs)) {
$lang = explode(';', $lang);
$lang = $lang[0];
$lang = str_replace('-', '_', $lang);
if (file_exists(INSTALL_PATH . "/locale/$lang.php")) {
return $lang;
}
if (isset($aliases[$lang]) && ($alias = $aliases[$lang])
&& file_exists(INSTALL_PATH . "/locale/$alias.php")
) {
return $alias;
}
}
return null;
}
/**
* User authentication (and authorization).
*/
private function auth()
{
if (isset($_POST['login'])) {
$login = $this->get_input('login', 'POST');
if ($login['username']) {
$result = $this->api->login($login['username'], $login['password'], $login['domain']);
if ($token = $result->get('session_token')) {
$user = array(
'token' => $token,
'id' => $result->get('userid'),
'domain' => $result->get('domain')
);
$this->api->set_session_token($user['token']);
// Find user settings
// Don't call API user.info for non-existing users (#1025)
if (preg_match('/^cn=([a-z ]+)/i', $login['username'], $m)) {
$user['fullname'] = ucwords($m[1]);
}
else {
$res = $this->api->get('user.info', array('id' => $user['id']));
$res = $res->get();
if (is_array($res) && !empty($res)) {
$user['language'] = $res['preferredlanguage'];
$user['fullname'] = $res['cn'];
// overwrite user id set in login request, which is user base DN,
// with unique attribute, which suits better to our needs
$user['id'] = $res['id'];
}
}
// Save user data
$_SESSION['user'] = $user;
if (($language = $this->get_language()) && $language != 'en_US') {
$_SESSION['user']['language'] = $language;
$session_config['language'] = $language;
}
// Configure API session
if (!empty($session_config)) {
$this->api->post('system.configure', null, $session_config);
}
header('Location: ?');
die;
}
else {
$code = $result->get_error_code();
$str = $result->get_error_str();
$label = 'loginerror';
if ($code == kolab_client_api::ERROR_INTERNAL
|| $code == kolab_client_api::ERROR_CONNECTION
) {
$label = 'internalerror';
$this->raise_error(500, 'Login failed. ' . $str);
}
$this->output->command('display_message', $label, 'error');
}
}
}
else if (!empty($_SESSION['user']) && !empty($_SESSION['user']['token'])) {
// Validate session
$timeout = $this->config_get('session_timeout', 3600);
if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) {
$this->action_logout(true);
}
// update session time
$_SESSION['time'] = time();
// Set API session key
$this->api->set_session_token($_SESSION['user']['token']);
}
}
/**
* Main execution.
*/
public function run()
{
// Initialize locales
$this->locale_init();
// Session check
if (empty($_SESSION['user']) || empty($_SESSION['user']['token'])) {
$this->action_logout();
}
// Run security checks
$this->input_checks();
$this->action = $this->get_input('action', 'GET');
if ($this->action) {
$method = 'action_' . $this->action;
if (method_exists($this, $method)) {
$this->$method();
}
}
else if (method_exists($this, 'action_default')) {
$this->action_default();
}
}
/**
* Security checks and input validation.
*/
public function input_checks()
{
$ajax = $this->output->is_ajax();
// Check AJAX-only tasks
if ($this->ajax_only && !$ajax) {
$this->raise_error(500, 'Invalid request type!', null, true);
}
// CSRF prevention
$token = $ajax ? kolab_utils::get_request_header('X-Session-Token') : $this->get_input('token');
$task = $this->get_task();
if ($task != 'main' && $token != $_SESSION['user']['token']) {
$this->raise_error(403, 'Invalid request data!', null, true);
}
}
/**
* Logout action.
*/
private function action_logout($sess_expired = false, $stop_sess = true)
{
// Initialize locales
$this->locale_init();
if (!empty($_SESSION['user']) && !empty($_SESSION['user']['token']) && $stop_sess) {
$this->api->logout();
}
$_SESSION = array();
if ($this->output->is_ajax()) {
if ($sess_expired) {
$args = array('error' => 'session.expired');
}
$this->output->command('main_logout', $args);
if ($sess_expired) {
$this->output->send();
exit;
}
}
else {
$this->output->add_translation('loginerror', 'internalerror', 'session.expired');
}
if ($sess_expired) {
$error = 'session.expired';
}
else {
$error = $this->get_input('error', 'GET');
}
if ($error) {
$this->output->command('display_message', $error, 'error', 60000);
}
$this->send('login');
exit;
}
/**
* Error action (with error logging).
*
* @param int $code Error code
* @param string $msg Error message
* @param array $args Optional arguments (type, file, line)
* @param bool $output Enable to send output and finish
*/
public function raise_error($code, $msg, $args = array(), $output = false)
{
$log_line = sprintf("%s Error: %s (%s)",
isset($args['type']) ? $args['type'] : 'PHP',
$msg . (isset($args['file']) ? sprintf(' in %s on line %d', $args['file'], $args['line']) : ''),
$_SERVER['REQUEST_METHOD']);
write_log('errors', $log_line);
if (!$output) {
return;
}
if ($this->output->is_ajax()) {
header("HTTP/1.0 $code $msg");
die;
}
$this->output->assign('error_code', $code);
$this->output->assign('error_message', $msg);
$this->send('error');
exit;
}
/**
* Output sending.
*/
public function send($template = null)
{
if (!$template) {
$template = $this->get_task();
}
if ($this->page_title) {
$this->output->assign('pagetitle', $this->page_title);
}
$this->output->send($template);
exit;
}
/**
* Returns name of the current task.
*
* @return string Task name
*/
public function get_task()
{
$class_name = get_class($this);
if (preg_match('/^kolab_client_task_([a-z]+)$/', $class_name, $m)) {
return $m[1];
}
}
/**
* Returns output environment variable value
*
* @param string $name Variable name
*
* @return mixed Variable value
*/
public function get_env($name)
{
return $this->output->get_env($name);
}
/**
* Returns configuration option value.
*
* @param string $name Option name
* @param mixed $fallback Default value
* @param int $type Value type (one of Conf class constants)
*
* @return mixed Option value
*/
public function config_get($name, $fallback = null, $type = null)
{
$value = $this->config->get('kolab_wap', $name, $type);
return $value !== null ? $value : $fallback;
}
/**
* Returns translation of defined label/message.
*
* @return string Translated string.
*/
public static function translate()
{
$args = func_get_args();
if (is_array($args[0])) {
$args = $args[0];
}
$label = $args[0];
if (isset(self::$translation[$label])) {
$content = trim(self::$translation[$label]);
}
else {
$content = $label;
}
for ($i = 1, $len = count($args); $i < $len; $i++) {
$content = str_replace('$'.$i, $args[$i], $content);
}
return $content;
}
/**
* Returns input parameter value.
*
* @param string $name Parameter name
* @param string $type Parameter type (GET|POST|NULL)
* @param bool $allow_html Disables stripping of insecure content (HTML tags)
*
* @see kolab_utils::get_input
* @return mixed Input value.
*/
public static function get_input($name, $type = null, $allow_html = false)
{
if ($type == 'GET') {
$type = kolab_utils::REQUEST_GET;
}
else if ($type == 'POST') {
$type = kolab_utils::REQUEST_POST;
}
else {
$type = kolab_utils::REQUEST_ANY;
}
$result = kolab_utils::get_input($name, $type, $allow_html);
return $result;
}
/**
* Returns task menu output.
*
* @return string HTML output
*/
protected function menu()
{
if (empty($this->menu)) {
return '';
}
$menu = array();
$task = $this->get_task();
$caps = (array) $this->get_capability('actions');
foreach ($this->menu as $idx => $label) {
if (in_array($task, array('domain', 'group', 'resource', 'role', 'user'))) {
if (!array_key_exists($task . "." . $idx, $caps)) {
continue;
}
}
if (strpos($idx, '.')) {
$action = $idx;
$class = preg_replace('/\.[a-z_-]+$/', '', $idx);
}
else {
$action = $task . '.' . $idx;
$class = $idx;
}
$menu[$idx] = sprintf('<li class="%s">'
.'<a href="#%s" onclick="return kadm.command(\'%s\', \'\', this)">%s</a></li>',
$class, $idx, $action, $this->translate($label));
}
return '<ul>' . implode("\n", $menu) . '</ul>';
}
/**
* Adds watermark page definition into main page.
*/
protected function watermark($name)
{
$this->output->command('set_watermark', $name);
}
/**
* API GET request wrapper
*/
protected function api_get($action, $get = array())
{
return $this->api_call('get', $action, $get);
}
/**
* API POST request wrapper
*/
protected function api_post($action, $get = array(), $post = array())
{
return $this->api_call('post', $action, $get, $post);
}
/**
* API request wrapper with error handling
*/
protected function api_call($type, $action, $get = array(), $post = array())
{
if ($type == 'post') {
$result = $this->api->post($action, $get, $post);
}
else {
$result = $this->api->get($action, $get);
}
// error handling
if ($code = $result->get_error_code()) {
// Invalid session, do logout
if ($code == 403) {
$this->action_logout(true, false);
}
// Log communication errors, other should be logged on API side
if ($code < 400) {
$this->raise_error($code, 'API Error: ' . $result->get_error_str());
}
}
return $result;
}
/**
* Returns list of object types.
*
* @para string $type Object type name
* @param string $used_for Used_for attribute of object type
*
* @return array List of user types
*/
protected function object_types($type, $used_for = null)
{
if (empty($type) || !in_array($type, $this->object_types)) {
return array();
}
$cache_idx = $type . '_types' . ($used_for ? ":$used_for" : '');
if (!array_key_exists($cache_idx, $this->cache)) {
$result = $this->api_post($type . '_types.list');
$list = $result->get('list');
if (!empty($used_for) && is_array($list)) {
foreach ($list as $type_id => $type_attrs) {
if ($type_attrs['used_for'] != $used_for) {
unset($list[$type_id]);
}
}
}
$this->cache[$cache_idx] = !empty($list) ? $list : array();
Log::trace("kolab_client_task::${type}_types() returns: " . var_export($list, true));
}
return $this->cache[$cache_idx];
}
/**
* Returns user name.
*
* @param string $dn User DN attribute value
*
* @return string User name (displayname)
*/
protected function user_name($dn)
{
if (!$this->devel_mode) {
if (!empty($this->cache['user_names']) && isset($this->cache['user_names'][$dn])) {
return $this->cache['user_names'][$dn];
}
}
$result = $this->api_get('user.info', array('id' => $dn));
$username = $result->get('displayname');
if (empty($username)) {
$username = $result->get('cn');
}
if (empty($username)) {
if (preg_match('/^cn=([a-zA=Z ]+)/', $dn, $m)) {
$username = ucwords($m[1]);
}
}
if (!$this->devel_mode) {
$this->cache['user_names'][$dn] = $username;
}
return $username;
}
/**
* Returns list of system capabilities.
*
* @param bool $all If enabled capabilities for all domains will be returned
* @param bool $refresh Disable session cache
*
* @return array List of system capabilities
*/
protected function capabilities($all = false, $refresh = false)
{
if (!$refresh && isset($_SESSION['capabilities']) && !$this->devel_mode) {
$list = $_SESSION['capabilities'];
}
else {
$result = $this->api_post('system.capabilities');
$list = $result->get('list');
if (is_array($list) && !$this->devel_mode) {
$_SESSION['capabilities'] = $list;
}
}
$domain = $this->domain ? $this->domain : $_SESSION['user']['domain'];
return !$all ? $list[$domain] : $list;
}
/**
* Returns system capability
*
* @param string $name Capability (key) name
*
* @return array Capability value if supported, NULL otherwise
*/
protected function get_capability($name)
{
$caps = $this->capabilities();
return $caps[$name];
}
/**
* Returns domains list (based on capabilities response)
*
* @param bool $refresh Refresh session cache
*
* @return array List of domains
*/
protected function get_domains($refresh = false)
{
$caps = $this->capabilities(true, $refresh);
return is_array($caps) ? array_keys($caps) : array();
}
/**
* Returns effective rights for the specified object
*
* @param string $type Object type
* @param string $id Object identifier
*
* @return array Two element array with 'attribute' and 'entry' elements
*/
protected function effective_rights($type, $id = null)
{
$caps = $this->get_capability('actions');
if (empty($caps[$type . '.effective_rights'])) {
return array(
'attribute' => array(),
'entry' => array(),
);
}
// Get the rights on the entry and attribute level
$result = $this->api_get($type . '.effective_rights', array('id' => $id));
$result = array(
'attribute' => $result->get('attributeLevelRights'),
'entry' => $result->get('entryLevelRights'),
);
return $result;
}
/**
* Returns execution time in seconds
*
* @param string Execution time
*/
public function gentime()
{
return sprintf('%.4f', microtime(true) - KADM_START);
}
/**
* Returns HTML output of login form
*
* @param string HTML output
*/
public function login_form()
{
$post = $this->get_input('login', 'POST');
$username = kolab_html::label(array(
'for' => 'login_name',
'content' => $this->translate('login.username')), true)
. kolab_html::input(array(
'type' => 'text',
'id' => 'login_name',
'name' => 'login[username]',
'value' => $post['username'],
'autofocus' => true));
$password = kolab_html::label(array(
'for' => 'login_pass',
'content' => $this->translate('login.password')), true)
. kolab_html::input(array(
'type' => 'password',
'id' => 'login_pass',
'name' => 'login[password]',
'value' => ''));
$button = kolab_html::input(array(
'type' => 'submit',
'id' => 'login_submit',
'value' => $this->translate('login.login')));
$form = kolab_html::form(array(
'id' => 'login_form',
'name' => 'login',
'method' => 'post',
'action' => '?'),
kolab_html::span(array('content' => $username))
. kolab_html::span(array('content' => $password))
. $button);
return $form;
}
/**
* Returns form element definition based on field attributes
*
* @param array $field Field attributes
* @param array $data Attribute values
*
* @return array Field definition
*/
protected function form_element_type($field, $data = array())
{
$result = array();
switch ($field['type']) {
case 'select':
case 'multiselect':
$opts = $this->form_element_select_data($field, $data);
$result['type'] = kolab_form::INPUT_SELECT;
$result['options'] = $opts['options'];
$result['value'] = $opts['default'];
$result['default'] = $field['default'];
if ($field['type'] == 'multiselect') {
$result['multiple'] = true;
}
break;
case 'list':
$result['type'] = kolab_form::INPUT_TEXTAREA;
$result['data-type'] = kolab_form::TYPE_LIST;
if (!empty($field['maxlength'])) {
$result['data-maxlength'] = $field['maxlength'];
}
if (!empty($field['maxcount'])) {
$result['data-maxcount'] = $field['maxcount'];
}
if (!empty($field['autocomplete'])) {
$result['data-autocomplete'] = true;
}
break;
case 'checkbox':
$result['type'] = kolab_form::INPUT_CHECKBOX;
break;
case 'password':
$result['type'] = kolab_form::INPUT_PASSWORD;
if (isset($field['maxlength'])) {
$result['maxlength'] = $field['maxlength'];
}
break;
case 'text-quota':
$result['type'] = kolab_form::INPUT_TEXTQUOTA;
$result['default'] = $field['default'];
break;
default:
$result['type'] = kolab_form::INPUT_TEXT;
if (isset($field['maxlength'])) {
$result['maxlength'] = $field['maxlength'];
}
if ($field['type'] && $field['type'] != 'text') {
$result['data-type'] = $field['type'];
if ($field['type'] == 'ldap_url') {
$this->output->add_translation('ldap.one', 'ldap.sub', 'ldap.base',
'ldap.host', 'ldap.basedn','ldap.scope', 'ldap.conditions',
'ldap.filter_any', 'ldap.filter_both', 'ldap.filter_prefix', 'ldap.filter_suffix',
'ldap.filter_exact'
);
}
}
else {
$result['default'] = $field['default'];
}
}
$result['required'] = empty($field['optional']);
return $result;
}
/**
* Prepares options/value of select element
*
* @param array $field Field attributes
* @param array $data Attribute values
* @param bool $lc Convert option values to lower-case
*
* @return array Options/Default definition
*/
protected function form_element_select_data($field, $data = array(), $lc = false)
{
$options = array();
$default = null;
if (!isset($field['values'])) {
$data['attributes'] = array($field['name']);
$resp = $this->api_post('form_value.select_options', null, $data);
$resp = $resp->get($field['name']);
unset($data['attributes']);
$default = $resp['default'];
$field['values'] = $resp['list'];
}
if (!empty($field['values'])) {
if ($lc) {
$options = array_combine(array_map('strtolower', $field['values']), $field['values']);
}
else {
$options = array_combine($field['values'], $field['values']);
}
// Exceptions
if ($field['name'] == 'ou') {
foreach ($options as $idx => $dn) {
$options[$idx] = kolab_utils::dn2ufn($dn);
}
}
}
return array(
'options' => $options,
'default' => $default,
);
}
/**
* HTML Form elements preparation.
*
* @param string $name Object name (user, group, etc.)
* @param array $data Object data
* @param array $extra_fields Extra field names
*
* @return array Fields list, Object types list, Current type ID
*/
protected function form_prepare($name, &$data, $extra_fields = array(), $used_for = null)
{
$types = (array) $this->object_types($name, $used_for);
$add_mode = empty($data['id']);
$event_fields = array();
$auto_fields = array();
$form_fields = array();
$fields = array();
$auto_attribs = array();
$extra_fields = array_flip($extra_fields);
// Object type
$data['object_type'] = $name;
// Selected account type
if (!empty($data['type_id'])) {
$type = $data['type_id'];
}
else {
$data['type_id'] = $type = key($types);
}
if ($type) {
$auto_fields = (array) $types[$type]['attributes']['auto_form_fields'];
$form_fields = (array) $types[$type]['attributes']['form_fields'];
}
// Mark automatically generated fields as read-only, etc.
foreach ($auto_fields as $idx => $field) {
//console("\$field value for \$auto_fields[\$idx] (idx: $idx)", $auto_fields[$idx]);
if (!is_array($field)) {
//console("not an array... unsetting");
unset($auto_fields[$idx]);
continue;
}
// merge with field definition from
if (isset($form_fields[$idx])) {
$field = array_merge($field, $form_fields[$idx]);
}
// remove auto-generated value on type change, it will be re-generated
else if ($add_mode) {
unset($data[$idx]);
}
$field['name'] = $idx;
$fields[$idx] = $this->form_element_type($field, $data);
$fields[$idx]['readonly'] = true;
$extra_fields[$idx] = true;
// build auto_attribs and event_fields lists
$is_data = 0;
if (!empty($field['data'])) {
foreach ($field['data'] as $fd) {
$event_fields[$fd][] = $idx;
if (isset($data[$fd])) {
$is_data++;
}
}
if (count($field['data']) == $is_data) {
$auto_attribs[] = $idx;
}
}
else {
//console("\$field['data'] is empty for \$auto_fields[\$idx] (idx: $idx)");
$auto_attribs[] = $idx;
// Unset the $auto_field array key to prevent the form field from
// becoming disabled/readonly
unset($auto_fields[$idx]);
}
}
// Other fields
foreach ($form_fields as $idx => $field) {
if (!isset($fields[$idx])) {
$field['name'] = $idx;
$fields[$idx] = $this->form_element_type($field, $data);
}
else {
unset($extra_fields[$idx]);
}
$fields[$idx]['readonly'] = false;
// Attach on-change events to some fields, to update
// auto-generated field values
if (!empty($event_fields[$idx])) {
$event = json_encode(array_unique($event_fields[$idx]));
$fields[$idx]['onchange'] = "kadm.form_value_change($event)";
}
}
// Get the rights on the entry and attribute level
$data['effective_rights'] = $this->effective_rights($name, $data['id']);
$attribute_rights = (array) $data['effective_rights']['attribute'];
$entry_rights = (array) $data['effective_rights']['entry'];
// See if "administrators" (those who can delete and add back on the entry
// level) may override the automatically generated contents of auto_form_fields.
$admin_auto_fields_rw = $this->config_get('admin_auto_fields_rw', false, Conf::BOOL);
foreach ($fields as $idx => $field) {
if (!array_key_exists($idx, $attribute_rights)) {
// If the entry level rights contain 'add' and 'delete', well, you're an admin
if (in_array('add', $entry_rights) && in_array('delete', $entry_rights)) {
if ($admin_auto_fields_rw) {
$fields[$idx]['readonly'] = false;
}
}
else {
$fields[$idx]['readonly'] = true;
}
}
else {
if (in_array('add', $entry_rights) && in_array('delete', $entry_rights)) {
if ($admin_auto_fields_rw) {
$fields[$idx]['readonly'] = false;
}
}
// Explicit attribute level rights, check for 'write'
elseif (!in_array('write', $attribute_rights[$idx])) {
$fields[$idx]['readonly'] = true;
}
}
}
// Register list of auto-generated fields
$this->output->set_env('auto_fields', $auto_fields);
// Register list of disabled fields
$this->output->set_env('extra_fields', array_keys($extra_fields));
// (Re-|Pre-)populate auto_form_fields
if ($add_mode) {
if (!empty($auto_attribs)) {
$data['attributes'] = $auto_attribs;
$resp = $this->api_post('form_value.generate', null, $data);
$data = array_merge((array)$data, (array)$resp->get());
unset($data['attributes']);
}
}
else {
// Add common information fields
$add_fields = array(
'creatorsname' => 'createtimestamp',
'modifiersname' => 'modifytimestamp',
);
foreach ($add_fields as $idx => $val) {
if (!empty($data[$idx])) {
if ($value = $this->user_name($data[$idx])) {
if ($data[$val]) {
$value .= ' (' . strftime('%x %X', strtotime($data[$val])) . ')';
}
$fields[$idx] = array(
'label' => $idx,
'section' => 'system',
'value' => $value,
);
}
}
}
// Add debug information
if ($this->devel_mode) {
ksort($data);
$debug = kolab_html::escape(print_r($data, true));
$debug = preg_replace('/(^Array\n\(|\n*\)$|\t)/', '', $debug);
$debug = str_replace("\n ", "\n", $debug);
$debug = '<pre class="debug">' . $debug . '</pre>';
$fields['debug'] = array(
'label' => 'debug',
'section' => 'system',
'value' => $debug,
);
}
}
// Add object type hidden field
$fields['object_type'] = array(
'section' => 'system',
'type' => kolab_form::INPUT_HIDDEN,
'value' => $name,
);
// Get user-friendly names for lists
foreach ($fields as $fname => $field) {
if (!empty($field['data-autocomplete']) && !empty($data[$fname])) {
if (!is_array($data[$fname])) {
$data[$fname] = (array) $data[$fname];
}
//console("The data for field $fname at this point is", $data[$fname]);
// request parameters
$post = array(
'list' => $data[$fname],
'attribute' => $fname,
'object_type' => $name,
'type_id' => $data['type_id'],
);
// get options list
$result = $this->api_post('form_value.list_options', null, $post);
$result = $result->get('list');
$data[$fname] = $result;
//console("Set \$data['$fname'] to", $result);
}
}
// Add entry identifier
if (!$add_mode) {
$fields['id'] = array(
'section' => 'system',
'type' => kolab_form::INPUT_HIDDEN,
'value' => $data['id']
);
}
$result = array($fields, $types, $type);
return $result;
}
/**
* HTML Form creation.
*
* @param string $name Object name (user, group, etc.)
* @param array $attribs HTML attributes of the form
* @param array $sections List of form sections
* @param array $fields Fields list (from self::form_prepare())
* @param array $fields_map Fields map (used for sorting and sections assignment)
* @param array $data Object data (with effective rights, see form_prepare())
*
* @return kolab_form HTML Form object
*/
protected function form_create($name, $attribs, $sections, $fields, $fields_map, $data, $add_mode)
{
//console("Creating form for $name with data", $data);
//console("Assign fields to sections", $fields);
// Assign sections to fields
foreach ($fields as $idx => $field) {
if (!$field['section']) {
$fields[$idx]['section'] = isset($fields_map[$idx]) ? $fields_map[$idx] : 'other';
//console("Assigned field $idx to section " . $fields[$idx]['section']);
}
}
//console("Using fields_map", $fields_map);
// Sort
foreach ($fields_map as $idx => $val) {
if (array_key_exists($idx, $fields)) {
$fields_map[$idx] = $fields[$idx];
unset($fields[$idx]);
}
else {
unset($fields_map[$idx]);
}
}
if (!empty($fields)) {
$fields_map = array_merge($fields_map, $fields);
}
//console("Using attribs", $attribs);
$form = new kolab_form($attribs);
$assoc_fields = array();
$req_fields = array();
$writeable = 0;
$auto_fields = $this->output->get_env('auto_fields');
//console("form_create() \$attribs", $attribs);
//console("form_create() \$auto_fields", $auto_fields);
//console("Going to walk through sections", $sections);
// Parse elements and add them to the form object
foreach ($sections as $section_idx => $section) {
$form->add_section($section_idx, kolab_html::escape($this->translate($section)));
foreach ($fields_map as $idx => $field) {
if ($field['section'] != $section_idx) {
continue;
}
if (empty($field['label'])) {
$field['label'] = "$name.$idx";
}
$field['label'] = kolab_html::escape($this->translate($field['label']));
$field['description'] = "$name.$idx.desc";
$field['section'] = $section_idx;
if (empty($field['value']) && !empty($data[$idx])) {
//console("Using data value", $data[$idx], "for value of field $idx");
$value = $data[$idx];
// Convert data for the list field with autocompletion
if ($field['data-type'] == kolab_form::TYPE_LIST) {
if (!is_array($value)) {
if (!empty($field['data-autocomplete'])) {
$value = array($value => $value);
}
else {
$value = (array) $value;
}
}
$value = !empty($field['data-autocomplete']) ? array_keys($value) : array_values($value);
}
if (is_array($value)) {
$value = implode("\n", $value);
}
$field['value'] = $value;
}
else if ($add_mode && !isset($field['value']) && isset($field['default'])) {
$field['value'] = $field['default'];
unset($field['default']);
}
// @TODO: We assume here that all autocompletion lists are associative
// It's likely that we'll need autocompletion on ordinary lists
if (!empty($field['data-autocomplete'])) {
$assoc_fields[$idx] = !empty($data[$idx]) ? $data[$idx] : array();
}
+
+ if ($field['type'] == kolab_form::INPUT_CHECKBOX) {
+ $field['checked'] = $field['value'] == 'TRUE';
+ }
/*
if (!empty($field['suffix'])) {
$field['suffix'] = kolab_html::escape($this->translate($field['suffix']));
}
*/
if (!empty($field['options'])) {
foreach ($field['options'] as $opt_idx => $option) {
if (is_array($option)) {
$field['options'][$opt_idx]['content'] = kolab_html::escape($this->translate($option['content']));
}
else {
$field['options'][$opt_idx] = kolab_html::escape($this->translate($option));
}
}
}
if (!empty($field['description'])) {
$description = $this->translate($field['description']);
if ($description != $field['description']) {
$field['title'] = $description;
}
unset($field['description']);
}
if (empty($field['name'])) {
$field['name'] = $idx;
}
if (empty($field['readonly']) && empty($field['disabled'])) {
// count writeable fields
if ($field['type'] && $field['type'] != kolab_form::INPUT_HIDDEN) {
$writeable++;
}
if ($idx != "userpassword2") {
if (!empty($field['required'])) {
$req_fields[] = $idx;
}
}
}
//console("Adding field to form", $field);
$form->add_element($field);
}
}
if (!empty($data['section'])) {
$form->activate_section($data['section']);
}
if ($writeable) {
$form->add_button(array(
'value' => kolab_html::escape($this->translate('button.submit')),
'onclick' => "kadm.{$name}_save()",
));
}
if (!empty($data['id']) && in_array('delete', (array) $data['effective_rights']['entry'])) {
$id = $data['id'];
// disable delete for self
if ($id != $_SESSION['user']['id']) {
$form->add_button(array(
'value' => kolab_html::escape($this->translate('button.delete')),
'onclick' => "kadm.{$name}_delete('{$id}')",
));
}
}
$ac_min_len = $this->config_get('autocomplete_min_length', 1, Conf::INT);
$this->output->set_env('form_id', $attribs['id']);
$this->output->set_env('assoc_fields', $assoc_fields);
$this->output->set_env('required_fields', $req_fields);
$this->output->set_env('autocomplete_min_length', $ac_min_len);
$this->output->add_translation('form.required.empty', 'form.maxcount.exceeded',
$name . '.add.success', $name . '.edit.success', $name . '.delete.success',
'add', 'edit', 'delete');
return $form;
}
/**
* Search form.
*
* @return string HTML output of the form
*/
public function search_form()
{
$attribs = $this->get_search_attribs();
if (empty($attribs)) {
return '';
}
$options = array();
foreach (array_keys($attribs) as $key) {
$options[$key] = kolab_html::escape($this->translate('search.' . $key));
}
$form = new kolab_form(array('id' => 'search-form'));
$form->add_section('criteria', kolab_html::escape($this->translate('search.criteria')));
$form->add_element(array(
'section' => 'criteria',
'label' => $this->translate('search.field'),
'name' => 'field',
'type' => kolab_form::INPUT_SELECT,
'options' => $options,
));
$form->add_element(array(
'section' => 'criteria',
'label' => $this->translate('search.method'),
'name' => 'method',
'type' => kolab_form::INPUT_SELECT,
'options' => array(
'both' => kolab_html::escape($this->translate('search.contains')),
'exact' => kolab_html::escape($this->translate('search.is')),
'prefix' => kolab_html::escape($this->translate('search.prefix')),
),
));
return $form->output();
}
/**
* Returns list of attributes assigned to search field(s)
*
* @param string $name Optional search field name
*
* @return array List of attributes
*/
protected function get_search_attribs($name = null)
{
$task = $this->get_task();
$types = $this->object_types($task);
$attribs = array();
foreach ($this->search_attribs as $key => $value) {
foreach ((array)$value as $idx => $attr) {
$found = false;
foreach ($types as $type) {
if (array_key_exists($attr, (array)$type['attributes']['auto_form_fields'])
|| array_key_exists($attr, (array)$type['attributes']['form_fields'])
|| array_key_exists($attr, (array)$type['attributes']['fields'])
) {
$found = true;
break;
}
}
if (!$found) {
unset($value[$idx]);
}
}
if (!empty($value)) {
$attribs[$key] = $value;
}
}
return $name ? $attribs[$name] : $attribs;
}
/**
* Object list (and search handler)
*/
public function action_list()
{
if (empty($this->list_attribs)) {
return;
}
$task = $this->get_task();
$page = (int) self::get_input('page', 'POST');
if (!$page || $page < 1) {
$page = 1;
}
// request parameters
$post = array(
'attributes' => $this->list_attribs,
'sort_by' => $this->list_attribs,
// 'sort_order' => 'ASC',
'page_size' => $this->page_size,
'page' => $page,
);
// search parameters
if (!empty($_POST['search'])) {
$search = self::get_input('search', 'POST', true);
$field = self::get_input('field', 'POST');
$method = self::get_input('method', 'POST');
$attrs = $this->get_search_attribs($field);
$search_request = array();
foreach ($attrs as $attr) {
$search_request[$attr] = array(
'value' => $search,
'type' => $method,
);
}
}
else if (!empty($_POST['search_request'])) {
$search_request = self::get_input('search_request', 'POST');
$search_request = @unserialize(base64_decode($search_request));
}
if (!empty($search_request)) {
$post['search'] = $search_request;
$post['search_operator'] = 'OR';
}
// get users list
$module = $this->list_module ? $this->list_module : $task . 's';
$result = $this->api_post($module . '.list', null, $post);
$count = $result->get('count');
$result = (array) $result->get('list');
// calculate records
if ($count) {
$start = 1 + max(0, $page - 1) * $this->page_size;
$end = min($start + $this->page_size - 1, $count);
}
$rows = $head = $foot = array();
$cols = array('name');
$i = 0;
// table header
$head[0]['cells'][] = array('class' => 'name', 'body' => $this->translate($task . '.list'));
// table footer (navigation)
if ($count) {
$pages = ceil($count / $this->page_size);
$prev = max(0, $page - 1);
$next = $page < $pages ? $page + 1 : 0;
$count_str = kolab_html::span(array(
'content' => $this->translate('list.records', $start, $end, $count)), true);
$prev = kolab_html::a(array(
'class' => 'prev' . ($prev ? '' : ' disabled'),
'href' => '#',
'onclick' => $prev ? "kadm.command('$task.list', {page: $prev})" : "return false",
));
$next = kolab_html::a(array(
'class' => 'next' . ($next ? '' : ' disabled'),
'href' => '#',
'onclick' => $next ? "kadm.command('$task.list', {page: $next})" : "return false",
));
$foot_body = kolab_html::span(array('content' => $prev . $count_str . $next));
}
$foot[0]['cells'][] = array('class' => 'listnav', 'body' => $foot_body);
// table body
if (!empty($result)) {
foreach ($result as $idx => $item) {
if (!is_array($item)) {
continue;
}
if (method_exists($this, 'list_item_handler')) {
$item = $this->list_item_handler($item);
}
else {
$item = array_shift($item);
}
if (empty($item)) {
continue;
}
$i++;
$cells = array();
$cells[] = array('class' => 'name', 'body' => kolab_html::escape($item),
'onclick' => "kadm.command('$task.info', '$idx')");
$rows[] = array('id' => $i, 'class' => 'selectable', 'cells' => $cells);
}
}
else {
$rows[] = array('cells' => array(
0 => array('class' => 'empty-body', 'body' => $this->translate($task . '.norecords')
)));
}
$table = kolab_html::table(array(
'id' => $task . 'list',
'class' => 'list',
'head' => $head,
'body' => $rows,
'foot' => $foot,
));
if ($this->action == 'list') {
$this->output->command('set_watermark', 'taskcontent');
}
$this->output->set_env('search_request', $search_request ? base64_encode(serialize($search_request)) : null);
$this->output->set_env('list_page', $page);
$this->output->set_env('list_count', $count);
$this->output->set_env('list_size', count($result));
$this->output->set_object($task . 'list', $table);
}
}
diff --git a/public_html/js/kolab_admin.js b/public_html/js/kolab_admin.js
index fac2aa6..a93d515 100644
--- a/public_html/js/kolab_admin.js
+++ b/public_html/js/kolab_admin.js
@@ -1,2310 +1,2315 @@
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab Web Admin Panel |
| |
| Copyright (C) 2011-2012, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 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 Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
function kolab_admin()
{
var ref = this;
this.env = {};
this.translations = {};
this.request_timeout = 300;
this.message_time = 3000;
this.events = {};
// set jQuery ajax options
$.ajaxSetup({
cache: false,
error: function(request, status, err) { ref.http_error(request, status, err); },
beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
});
/*********************************************************/
/********* basic utilities *********/
/*********************************************************/
// set environment variable(s)
this.set_env = function(p, value)
{
if (p != null && typeof p === 'object' && !value)
for (var n in p)
this.env[n] = p[n];
else
this.env[p] = value;
};
// add a localized label(s) to the client environment
this.tdef = function(p, value)
{
if (typeof p == 'string')
this.translations[p] = value;
else if (typeof p == 'object')
$.extend(this.translations, p);
};
// return a localized string
this.t = function(label)
{
if (this.translations[label])
return this.translations[label];
else
return label;
};
// print a message into browser console
this.log = function(msg)
{
if (window.console && console.log)
console.log(msg);
};
// execute a specific command on the web client
this.command = function(command, props, obj)
{
if (obj && obj.blur)
obj.blur();
if (this.busy)
return false;
this.set_busy(true, 'loading');
var ret = undefined,
func = command.replace(/[^a-z]/g, '_'),
task = command.replace(/\.[a-z-_]+$/g, '');
if (this[func] && typeof this[func] === 'function') {
ret = this[func](props);
}
else {
this.http_post(command, props);
}
// update menu state
$('li', $('#navigation')).removeClass('active');
$('li.'+task, ('#navigation')).addClass('active');
return ret === false ? false : obj ? false : true;
};
this.set_busy = function(a, message)
{
if (a && message) {
var msg = this.t(message);
if (msg == message)
msg = 'Loading...';
this.display_message(msg, 'loading');
}
else if (!a) {
this.hide_message('loading');
}
this.busy = a;
// if (this.gui_objects.editform)
// this.lock_form(this.gui_objects.editform, a);
// clear pending timer
if (this.request_timer)
clearTimeout(this.request_timer);
// set timer for requests
if (a && this.env.request_timeout)
this.request_timer = window.setTimeout(function() { ref.request_timed_out(); }, this.request_timeout * 1000);
};
// called when a request timed out
this.request_timed_out = function()
{
this.set_busy(false);
this.display_message('Request timed out!', 'error');
};
// Add variable to GET string, replace old value if exists
this.add_url = function(url, name, value)
{
value = urlencode(value);
if (/(\?.*)$/.test(url)) {
var urldata = RegExp.$1,
datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
if (datax.test(urldata))
urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
else
urldata += '&' + name + '=' + value
return url.replace(/(\?.*)$/, urldata);
}
else
return url + '?' + name + '=' + value;
};
this.trigger_event = function(event, data)
{
if (this.events[event])
for (var i in this.events[event])
this.events[event][i](data);
};
this.add_event_listener = function(event, func)
{
if (!this.events[event])
this.events[event] = [];
this.events[event].push(func);
};
/*********************************************************/
/********* GUI functionality *********/
/*********************************************************/
// write to the document/window title
this.set_pagetitle = function(title)
{
if (title && document.title)
document.title = title;
};
// display a system message (types: loading, notice, error)
this.display_message = function(msg, type, timeout)
{
var obj, ref = this;
if (!type)
type = 'notice';
if (msg)
msg = this.t(msg);
if (type == 'loading') {
timeout = this.request_timeout * 1000;
if (!msg)
msg = this.t('loading');
}
else if (!timeout)
timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
obj = $('<div>');
if (type != 'loading') {
msg = '<div><span>' + msg + '</span></div>';
obj.addClass(type).click(function() { return ref.hide_message(); });
}
if (timeout > 0)
window.setTimeout(function() { ref.hide_message(type, type != 'loading'); }, timeout);
obj.attr('id', type == 'loading' ? 'loading' : 'message')
.appendTo('body').html(msg).show();
};
// make a message to disapear
this.hide_message = function(type, fade)
{
if (type == 'loading')
$('#loading').remove();
else
$('#message').fadeOut('normal', function() { $(this).remove(); });
};
this.set_watermark = function(id)
{
if (this.env.watermark)
$('#'+id).html(this.env.watermark);
}
/********************************************************/
/********* Remote request methods *********/
/********************************************************/
// compose a valid url with the given parameters
this.url = function(action, query)
{
var k, param = {},
querystring = typeof query === 'string' ? '&' + query : '';
if (typeof action !== 'string')
query = action;
else if (!query || typeof query !== 'object')
query = {};
// overwrite task name
if (action) {
if (action.match(/^([a-z]+)/i))
query.task = RegExp.$1;
if (action.match(/[^a-z0-9-_]([a-z0-9-_]+)$/i))
query.action = RegExp.$1;
}
// remove undefined values
for (k in query) {
if (query[k] !== undefined && query[k] !== null)
param[k] = query[k];
}
return '?' + $.param(param) + querystring;
};
// send a http POST request to the server
this.http_post = function(action, postdata)
{
var url = this.url(action);
if (postdata && typeof postdata === 'object')
postdata.remote = 1;
else {
if (!postdata)
postdata = '';
postdata += '&remote=1';
}
this.set_request_time();
return $.ajax({
type: 'POST', url: url, data: postdata, dataType: 'json',
success: function(response) { kadm.http_response(response, action); },
error: function(o, status, err) { kadm.http_error(o, status, err); }
});
};
// send a http POST request to the API service
this.api_post = function(action, postdata, func)
{
var url = 'api/' + action;
if (!func) func = 'api_response';
this.set_request_time();
return $.ajax({
type: 'POST', url: url, data: JSON.stringify(postdata), dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function(response) { kadm[func](response); },
error: function(o, status, err) { kadm.http_error(o, status, err); }
});
};
// handle HTTP response
this.http_response = function(response, action)
{
var i;
if (!response)
return;
// set env vars
if (response.env)
this.set_env(response.env);
// we have translation labels to add
if (typeof response.labels === 'object')
this.tdef(response.labels);
// HTML page elements
if (response.objects)
for (i in response.objects)
$('#'+i).html(response.objects[i]);
this.update_request_time();
this.set_busy(false);
// if we get javascript code from server -> execute it
if (response.exec)
eval(response.exec);
response.action = action;
this.trigger_event('http-response', response);
};
// handle HTTP request errors
this.http_error = function(request, status, err)
{
var errmsg = request.statusText;
this.set_busy(false);
request.abort();
if (request.status && errmsg)
this.display_message(this.t('servererror') + ' (' + errmsg + ')', 'error');
};
this.api_response = function(response)
{
this.update_request_time();
this.set_busy(false);
if (!response || response.status != 'OK') {
// Logout on invalid-session error
if (response && response.code == 403)
this.main_logout();
else
this.display_message(response && response.reason ? response.reason : this.t('servererror'), 'error');
return false;
}
return true;
};
/********************************************************/
/********* Helper methods *********/
/********************************************************/
// disable/enable all fields of a form
this.lock_form = function(form, lock)
{
if (!form || !form.elements)
return;
var n, len, elm;
if (lock)
this.disabled_form_elements = [];
for (n=0, len=form.elements.length; n<len; n++) {
elm = form.elements[n];
if (elm.type == 'hidden')
continue;
// remember which elem was disabled before lock
if (lock && elm.disabled)
this.disabled_form_elements.push(elm);
// check this.disabled_form_elements before inArray() as a workaround for FF5 bug
// http://bugs.jquery.com/ticket/9873
else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
elm.disabled = lock;
}
};
this.set_request_time = function()
{
this.env.request_time = (new Date()).getTime();
};
// Update request time element
this.update_request_time = function()
{
if (this.env.request_time) {
var t = ((new Date()).getTime() - this.env.request_time)/1000,
el = $('#reqtime');
el.text(el.text().replace(/[0-9.,]+/, t));
}
};
// position and display popup
this.popup_show = function(e, popup)
{
var popup = $(popup),
pos = this.mouse_pos(e),
win = $(window),
w = popup.width(),
h = popup.height(),
left = pos.left - w,
top = pos.top;
if (top + h > win.height())
top -= h;
if (left + w > win.width())
left -= w;
popup.css({left: left + 'px', top: top + 'px'}).show();
e.stopPropagation();
};
// Return absolute mouse position of an event
this.mouse_pos = function(e)
{
if (!e) e = window.event;
var mX = (e.pageX) ? e.pageX : e.clientX,
mY = (e.pageY) ? e.pageY : e.clientY;
if (document.body && document.all) {
mX += document.body.scrollLeft;
mY += document.body.scrollTop;
}
if (e._offset) {
mX += e._offset.left;
mY += e._offset.top;
}
return { left:mX, top:mY };
};
/*********************************************************/
/********* keyboard autocomplete methods *********/
/*********************************************************/
this.ac_init = function(obj, props)
{
obj.keydown(function(e) { return kadm.ac_keydown(e, props); })
.attr('autocomplete', 'off');
};
// handler for keyboard events on autocomplete-fields
this.ac_keydown = function(e, props)
{
if (this.ac_timer)
clearTimeout(this.ac_timer);
var highlight, key = e.which;
switch (key) {
case 38: // arrow up
case 40: // arrow down
if (!this.ac_visible())
break;
var dir = key == 38 ? 1 : 0;
highlight = $('.selected', this.ac_pane).get(0);
if (!highlight)
highlight = this.ac_pane.__ul.firstChild;
if (highlight)
this.ac_select(dir ? highlight.previousSibling : highlight.nextSibling);
return e.stopPropagation();
case 9: // tab
if (e.shiftKey || !this.ac_visible()) {
this.ac_stop();
return;
}
case 13: // enter
if (!this.ac_visible())
return false;
// insert selected item and hide selection pane
this.ac_insert(this.ac_selected);
this.ac_stop();
return e.stopPropagation();
case 27: // escape
this.ac_stop();
return;
case 37: // left
case 39: // right
if (!e.shiftKey)
return;
}
// start timer
this.ac_timer = window.setTimeout(function() { kadm.ac_start(props); }, 500);
this.ac_input = e.target;
return true;
};
this.ac_visible = function()
{
return (this.ac_selected !== null && this.ac_selected !== undefined && this.ac_value);
};
this.ac_select = function(node)
{
if (!node)
return;
var current = $('.selected', this.ac_pane);
if (current.length)
current.removeClass('selected');
$(node).addClass('selected');
this.ac_selected = node._id;
};
// autocomplete search processor
this.ac_start = function(props)
{
var q = this.ac_input ? this.ac_input.value : null,
min = this.env.autocomplete_min_length,
old_value = this.ac_value,
ac = this.ac_data;
if (q === null)
return;
// trim query string
q = $.trim(q);
// Don't (re-)search if the last results are still active
if (q == old_value)
return;
// Stop and destroy last search
this.ac_stop();
if (q.length && q.length < min) {
this.display_message(this.t('search.acchars').replace('$min', min), 'notice', 2000);
return;
}
this.ac_value = q;
// ...string is empty
if (!q.length)
return;
// ...new search value contains old one, but the old result was empty
if (old_value && old_value.length && q.indexOf(old_value) == 0 && this.ac_result && !this.ac_result.length)
return;
var i, xhr, data = props,
action = props && props.action ? props.action : 'form_value.list_options';
this.ac_oninsert = props.oninsert;
data.search = q;
delete data['action'];
delete data['insert_func'];
this.display_message(this.t('search.loading'), 'loading');
xhr = this.api_post(action, data, 'ac_result');
this.ac_data = xhr;
};
this.ac_result = function(response)
{
// search stopped in meantime?
if (!this.ac_value)
return;
if (!this.api_response(response))
return;
// ignore this outdated search response
if (this.ac_input && response.result.search != this.ac_value)
return;
// display search results
var i, ul, li, text,
result = response.result.list,
pos = $(this.ac_input).offset(),
value = this.ac_value,
rx = new RegExp('(' + RegExp.escape(value) + ')', 'ig');
// create results pane if not present
if (!this.ac_pane) {
ul = $('<ul>');
this.ac_pane = $('<div>').attr('id', 'autocompletepane')
.css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
this.ac_pane.__ul = ul[0];
}
ul = this.ac_pane.__ul;
// reset content
ul.innerHTML = '';
// move the results pane right under the input box
this.ac_pane.css({left: (pos.left - 1)+'px', top: (pos.top + this.ac_input.offsetHeight - 1)+'px', display: 'none'});
// add each result line to the list
for (i in result) {
text = result[i];
li = document.createElement('LI');
li.innerHTML = text.replace(rx, '##$1%%').replace(/</g, '<').replace(/>/g, '>').replace(/##([^%]+)%%/g, '<b>$1</b>');
li.onmouseover = function() { kadm.ac_select(this); };
li.onmouseup = function() { kadm.ac_click(this) };
li._id = i;
ul.appendChild(li);
}
if (ul.childNodes.length) {
this.ac_pane.show();
// select the first
li = $('li:first', ul);
li.addClass('selected');
this.ac_selected = li.get(0)._id;
}
this.env.ac_result = result;
};
this.ac_click = function(node)
{
if (this.ac_input)
this.ac_input.focus();
this.ac_insert(node._id);
this.ac_stop();
};
this.ac_insert = function(id)
{
var val = this.env.ac_result[id];
if (typeof this.ac_oninsert == 'function')
this.ac_oninsert(id, val);
else
$(this.ac_input).val(val);
};
this.ac_blur = function()
{
if (this.ac_timer)
clearTimeout(this.ac_timer);
this.ac_input = null;
this.ac_stop();
};
this.ac_stop = function()
{
this.ac_selected = null;
this.ac_value = '';
if (this.ac_pane)
this.ac_pane.hide();
this.ac_destroy();
};
// Clears autocomplete data/requests
this.ac_destroy = function()
{
if (this.ac_data)
this.ac_data.abort();
this.ac_data = null;
this.ac_info = null;
};
/*********************************************************/
/********* Forms widgets *********/
/*********************************************************/
// Form initialization
this.form_init = function(id)
{
var form = $('#'+id);
this.trigger_event('form-load', id);
// replace some textarea fields with pretty/smart input lists
$('textarea[data-type="list"]', form)
.each(function() { kadm.form_list_element_wrapper(this); });
// create smart select fields
$('input[data-type="select"]', form)
.each(function() { kadm.form_select_element_wrapper(this); });
// create LDAP URL fields
$('input[data-type="ldap_url"]:not(:disabled):not([readonly])', form)
.each(function() { kadm.form_url_element_wrapper(this); });
};
// Form serialization
this.form_serialize = function(data)
{
var form = $(data.id);
// smart list fields
$('textarea[data-type="list"]:not(:disabled)', form).each(function() {
var i, v, value = [],
re = RegExp('^' + RegExp.escape(this.name) + '\[[0-9-]+\]$');
for (i in data.json) {
if (i.match(re)) {
if (v = $('input[name="'+i+'"]', form).val())
value.push(v);
delete data.json[i];
}
}
// autocompletion lists data is stored in env variable
if (kadm.env.assoc_fields[this.name]) {
value = [];
for (i in kadm.env.assoc_fields[this.name])
value.push(i);
}
data.json[this.name] = value;
});
// smart selects
$('input[data-type="select"]', form).each(function() {
delete data.json[this.name];
});
// LDAP URL fields
$('input[data-type="ldap_url"]:not(:disabled):not([readonly])', form).each(function() {
data.json = kadm.form_url_element_submit(this.name, data.json, form);
});
// quota inputs
$('input[data-type="quota"]', form).each(function() {
var unit = $('select[name="' + this.name + '-unit"]').val();
if (unit && this.value)
data.json[this.name] = this.value + unit;
delete data.json[this.name + '-unit'];
});
+ // checkbox inputs
+ $('input[type="checkbox"]', form).each(function() {
+ data.json[this.name] = this.checked ? 'TRUE' : 'FALSE';
+ });
+
return data;
};
// Form element update handler
this.form_element_update = function(data)
{
var elem = $('[name="'+data.name+'"]');
if (!elem.length)
return;
if (elem.attr('data-type') == 'list') {
// remove old wrapper
$('span[class="listarea"]', elem.parent()).remove();
// insert new list element
this.form_list_element_wrapper(elem.get(0));
}
};
// Replaces form element with smart list element
this.form_list_element_wrapper = function(form_element)
{
var i = 0, j = 0, list = [], elem, e = $(form_element),
form = form_element.form,
disabled = e.attr('disabled'),
readonly = e.attr('readonly'),
autocomplete = e.attr('data-autocomplete'),
maxlength = e.attr('data-maxlength'),
area = $('<span class="listarea"></span>');
e.hide();
if (autocomplete)
list = this.env.assoc_fields ? this.env.assoc_fields[form_element.name] : [];
else if (form_element.value)
list = form_element.value.split("\n");
// Need at least one element
if (!autocomplete || disabled || readonly) {
$.each(list, function() { i++; });
if (!i)
list = [''];
}
// Create simple list for readonly/disabled
if (disabled || readonly) {
area.addClass('readonly');
// add simple input rows
$.each(list, function(i, v) {
var elem = $('<input>');
elem.attr({
value: v,
disabled: disabled,
readonly: readonly,
name: form_element.name + '[' + (j++) + ']'
})
elem = $('<span class="listelement">').append(elem);
elem.appendTo(area);
});
}
// extended widget with add/remove buttons and/or autocompletion
else {
// add autocompletion input
if (autocomplete) {
elem = this.form_list_element(form, {
maxlength: maxlength,
autocomplete: autocomplete,
element: e
}, -1);
// Initialize autocompletion
var props = {attribute: form_element.name, oninsert: this.form_element_oninsert};
if (i = $('[name="type_id"]', form).val())
props.type_id = i;
if (i = $('[name="object_type"]', form).val())
props.object_type = i;
if (i = $('[name="id"]', form).val())
props.id = i;
this.ac_init(elem, props);
elem.appendTo(area);
area.addClass('autocomplete');
}
// add input rows
$.each(list, function(i, v) {
var elem = kadm.form_list_element(form, {
value: v,
key: i,
maxlength: maxlength,
autocomplete: autocomplete,
element: e
}, j++);
elem.appendTo(area);
});
}
area.appendTo(form_element.parentNode);
};
// Creates smart list element
this.form_list_element = function(form, data, idx)
{
var content, elem, input,
key = data.key,
orig = data.element
ac = data.autocomplete;
data.name = (orig ? orig.attr('name') : data.name) + '[' + idx + ']';
data.readonly = (ac && idx >= 0);
// remove internal attributes
delete data['element'];
delete data['autocomplete'];
delete data['key'];
// build element content
content = '<span class="listelement"><span class="actions">'
+ (!ac ? '<span title="" class="add"></span>' : ac && idx == -1 ? '<span title="" class="search"></span>' : '')
+ (!ac || idx >= 0 ? '<span title="" class="reset"></span>' : '')
+ '</span><input type="text" autocomplete="off"></span>';
elem = $(content);
input = $('input', elem);
// Set INPUT attributes
input.attr(data);
if (data.readonly)
input.addClass('readonly');
if (ac)
input.addClass('autocomplete');
// attach element creation event
if (!ac)
$('span[class="add"]', elem).click(function() {
var name = data.name.replace(/\[[0-9]+\]$/, ''),
span = $(this.parentNode.parentNode),
maxcount = $('textarea[name="'+name+'"]').attr('data-maxcount');
// check element count limit
if (maxcount && maxcount <= span.parent().children().length) {
alert(kadm.t('form.maxcount.exceeded'));
return;
}
var dt = (new Date()).getTime(),
elem = kadm.form_list_element(form, {name: name}, dt);
kadm.ac_stop();
span.after(elem);
$('input', elem).focus();
});
// attach element deletion event
if (!ac || idx >= 0)
$('span[class="reset"]', elem).click(function() {
var span = $(this.parentNode.parentNode),
name = data.name.replace(/\[[0-9]+\]$/, ''),
l = $('input[name^="' + name + '"]', form),
key = $(this).data('key');
if (l.length > 1 || $('input[name="' + name + '"]', form).attr('data-autocomplete'))
span.remove();
else
$('input', span).val('').focus();
// delete key from internal field representation
if (key !== undefined && kadm.env.assoc_fields[name])
delete kadm.env.assoc_fields[name][key];
kadm.ac_stop();
}).data('key', key);
return elem;
};
this.form_element_oninsert = function(key, val)
{
var elem, input = $(this.ac_input).get(0),
dt = (new Date()).getTime(),
span = $(input.parentNode),
name = input.name.replace(/\[-1\]$/, ''),
af = kadm.env.assoc_fields,
maxcount = $('textarea[name="'+name+'"]').attr('data-maxcount');
// reset autocomplete input
input.value = '';
// check element count limit
if (maxcount && maxcount <= span.parent().children().length - 1) {
alert(kadm.t('form.maxcount.exceeded'));
return;
}
// check if element doesn't exist on the list already
if (!af[name])
af[name] = {};
if (af[name][key])
return;
// add element
elem = kadm.form_list_element(input.form, {
name: name,
autocomplete: true,
value: val
}, dt);
span.after(elem);
// update field variable
af[name][key] = val;
};
// Replaces form element with smart select element
this.form_select_element_wrapper = function(form_element)
{
var e = $(form_element),
form = form_element.form,
name = form_element.name,
elem = $('#selectlabel_' + name),
area = $('<span class="listarea autocomplete select popup" id="selectarea_' + name + '"></span>'),
content = $('<span class="listcontent" id="selectcontent_' + name + '"></span>'),
list = this.env.assoc_fields ? this.env.assoc_fields[form_element.name] : [];
if (elem.length) {
$('#selectarea_' + name).remove();
$('#selectcontent_' + name).remove();
}
else {
elem = $('<span class="link" id="selectlabel_' + name + '"></span>')
.css({cursor: 'pointer'})
.click(function(e) {
var popup = $('span.listarea', this.parentNode);
kadm.popup_show(e, popup);
$('input', popup).val('').focus();
$('span.listcontent > span.listelement', popup).removeClass('selected').show();
})
.appendTo(form_element.parentNode);
}
elem.text(e.val());
if (list.length <= 1)
return;
if (form_element.type != 'hidden') e.hide();
elem = this.form_list_element(form, {
autocomplete: true,
element: e
}, -1);
elem.appendTo(area);
content.appendTo(area);
area.hide().appendTo(form_element.parentNode);
// popup events
$('input', area)
.click(function(e) {
// stop click on the popup
e.stopPropagation();
})
.keypress(function(e) {
// prevent form submission with Enter key
if (e.which == 13)
e.preventDefault();
})
.keydown(function(e) {
// block Up/Down arrow keys,
// in Firefox Up arrow moves cursor left
if (e.which == 38 || e.which == 40)
e.preventDefault();
})
.keyup(function(e) {
// filtering
var s = this.value,
options = $('span.listcontent > span.listelement', area);
// Enter key
if (e.which == 13) {
options.filter('.selected').click()
return;
}
// Escape
else if (e.which == 27) {
area.hide();
this.value = s = '';
}
// UP/Down arrows
else if (e.which == 38 || e.which == 40) {
options = options.not(':hidden');
if (options.length <= 1)
return;
var focused,
selected = options.filter('.selected'),
index = options.index(selected);
if (e.which == 40) {
if (!(focused = options.get(index+1)))
focused = options.get(index-1);
}
else {
if (!(focused = options.get(index-1)))
focused = options.get(index+1);
}
if (focused) {
focused = $(focused);
selected.removeClass('selected');
focused.addClass('selected');
var parent = focused.parent(),
parent_height = parent.height(),
parent_top = parent.get(0).scrollTop,
top = focused.offset().top - parent.offset().top,
height = focused.height();
if (top < 0)
parent.get(0).scrollTop = 0;
else if (top >= parent_height)
parent.get(0).scrollTop = top - parent_height + height + parent_top;
}
return;
}
if (!s) {
options.show().removeClass('selected');
return;
}
options.each(function() {
var o = $(this), v = o.data('value');
o[v.indexOf(s) != -1 ? 'show' : 'hide']().removeClass('selected');
});
options = options.not(':hidden');
if (options.length == 1)
options.addClass('selected');
});
// add option rows
$.each(list, function(i, v) {
var elem = kadm.form_select_option_element(form, {value: v, key: v, element: e});
elem.appendTo(content);
});
};
// Creates option element for smart select
this.form_select_option_element = function(form, data)
{
// build element content
var elem = $('<span class="listelement"></span>')
.data('value', data.key).text(data.value)
.click(function(e) {
var val = $(this).data('value'),
elem = $(data.element),
old_val = elem.val();
$('span.link', elem.parent()).text(val);
elem.val(val);
if (val != old_val)
elem.change();
});
return elem;
};
// Replaces form element with LDAP URL element
this.form_url_element_wrapper = function(form_element)
{
var i, e = $(form_element),
form = form_element.form,
name = form_element.name,
ldap = this.parse_ldap_url(e.val()) || {},
options = ['sub', 'one', 'base'],
div = $('<div class="ldap_url"><table></table></div>'),
host = $('<input type="text">').attr({name: 'ldap_host_'+name, size: 30, value: ldap.host, 'class': 'ldap_host'}),
port = $('<input type="text">').attr({name: 'ldap_port_'+name, size: 5, value: ldap.port || '389'}),
base = $('<input type="text">').attr({name: 'ldap_base_'+name, value: ldap.base}),
scope = $('<select>').attr({name: 'ldap_scope_'+name}),
filter = $('<ul>'),
row_host = $('<tr class="ldap_host"><td></td><td></td></tr>'),
row_base = $('<tr class="ldap_base"><td></td><td></td></tr>'),
row_scope = $('<tr class="ldap_scope"><td></td><td></td></tr>'),
row_filter = $('<tr class="ldap_filter"><td></td><td></td></tr>');
for (i in options)
$('<option>').val(options[i]).text(this.t('ldap.'+options[i])).appendTo(scope);
scope.val(ldap.scope);
for (i in ldap.filter)
filter.append(this.form_url_filter_element(name, i, ldap.filter[i]));
if (!$('li', filter).length)
filter.append(this.form_url_filter_element(name, 0, {}));
e.hide();
$('td:first', row_host).text(this.t('ldap.host'));
$('td:last', row_host).append(host).append($('<span>').text(':')).append(port);
$('td:first', row_base).text(this.t('ldap.basedn'));
$('td:last', row_base).append(base);
$('td:first', row_scope).text(this.t('ldap.scope'));
$('td:last', row_scope).append(scope);
$('td:first', row_filter).text(this.t('ldap.conditions'));
$('td:last', row_filter).append(filter);
$('table', div).append(row_host).append(row_base).append(row_scope).append(row_filter);
$(form_element).parent().append(div);
};
this.form_url_filter_element = function(name, idx, filter)
{
var options = ['any', 'both', 'prefix', 'suffix', 'exact'],
filter_type = $('<select>').attr({name: 'ldap_filter_type_'+name+'['+idx+']'}),
filter_name = $('<input type="text">').attr({name: 'ldap_filter_name_'+name+'['+idx+']'}),
filter_value = $('<input type="text">').attr({name: 'ldap_filter_value_'+name+'['+idx+']'}),
a_add = $('<a class="button add" href="#add"></a>').click(function() {
var dt = new Date().getTime();
$(this.parentNode.parentNode).append(kadm.form_url_filter_element(name, dt, {}));
}).attr({title: this.t('add')}),
a_del = $('<a class="button delete" href="#delete"></a>').click(function() {
if ($('li', this.parentNode.parentNode).length > 1)
$(this.parentNode).remove();
else {
$('input', this.parentNode).val('');
$('select', this.parentNode).val('any').change();
}
}).attr({title: this.t('delete')}),
li = $('<li>');
for (i in options)
$('<option>').val(options[i]).text(this.t('ldap.filter_'+options[i])).appendTo(filter_type);
if (filter.type)
filter_type.val(filter.type);
if (filter.name)
filter_name.val(filter.name);
if (filter.value)
filter_value.val(filter.value);
filter_type.change(function() {
filter_value.css({display: $(this).val() == 'any' ? 'none' : 'inline'});
}).change();
return li.append(filter_name).append(filter_type).append(filter_value)
.append(a_del).append(a_add);
};
// updates form data with LDAP URL (on form submit)
this.form_url_element_submit = function(name, data, form)
{
var i, rx = new RegExp('^ldap_(host|port|base|scope|filter_name|filter_type|filter_value)_'+name+'(\\[|$)');
for (i in data)
if (rx.test(i))
delete data[i];
data[name] = this.form_url_element_save(name, form);
return data;
};
// updates LDAP URL field
this.form_url_element_save = function(name, form)
{
var url, form = $(form), params = {
host: $('input[name="ldap_host_'+name+'"]', form).val(),
port: $('input[name="ldap_port_'+name+'"]', form).val(),
base: $('input[name="ldap_base_'+name+'"]', form).val(),
scope: $('select[name="ldap_scope_'+name+'"]', form).val(),
filter: []};
$('input[name^="ldap_filter_name_'+name+'"]', form).each(function() {
if (this.value && /\[([^\]]+)\]/.test(this.name)) {
var suffix = name + '[' + RegExp.$1 + ']',
type = $('select[name="ldap_filter_type_'+suffix+'"]', form).val(),
value = type == 'any' ? '' : $('input[name="ldap_filter_value_'+suffix+'"]', form).val();
params.filter.push({name: this.value, type: type, value: value, join: 'AND', level: 0});
}
});
url = this.build_ldap_url(params);
$('input[name="'+name+'"]').val(url);
return url;
};
/*********************************************************/
/********* Forms *********/
/*********************************************************/
this.serialize_form = function(id)
{
var i, v, json = {},
form = $(id),
query = form.serializeArray(),
extra = this.env.extra_fields || [];
for (i in query)
json[query[i].name] = query[i].value;
// read extra (disabled) fields
for (i=0; i<extra.length; i++)
if (v = $('[name="'+extra[i]+'"]', form).val())
json[extra[i]] = v;
// serializeArray() doesn't work properly for multi-select
$('select[multiple="multiple"]', form).each(function() {
var name = this.name;
json[name] = [];
$(':selected', this).each(function() {
json[name].push(this.value);
});
});
this.form_serialize({id: id, json: json});
return json;
};
this.form_value_change = function(events)
{
var i, j, e, elem, name, elem_name,
form = $('#'+this.env.form_id),
id = $('[name="id"]', form).val(),
type_id = $('[name="type_id"]', form).val(),
object_type = $('[name="object_type"]', form).val(),
data = {type_id: type_id, object_type: object_type, attributes: []};
if (id)
data.id = id;
this.set_busy(true, 'loading');
for (i=0; i<events.length; i++) {
name = events[i];
e = this.env.auto_fields[name];
if (!e)
continue;
data.attributes.push(name);
for (j=0; j<e.data.length; j++) {
elem_name = e.data[j];
if (!data[elem_name] && (elem = $('[name="'+elem_name+'"]', form)))
data[elem_name] = elem.val();
}
}
this.api_post('form_value.generate', data, 'form_value_response');
this.set_busy(false);
};
this.form_value_response = function(response)
{
var i, val, field;
if (!this.api_response(response))
return;
for (i in response.result) {
val = response.result[i];
field = $('[name="'+i+'"]');
// @TODO: indexed list support
if ($.isArray(val))
val = field.is('textarea') ? val.join("\n") : val.shift();
field.val(val);
this.form_element_update({name: i});
}
};
this.form_value_error = function(name)
{
$('[name="'+name+'"]', $('#'+this.env.form_id)).addClass('error');
}
this.form_error_clear = function()
{
$('input,textarea', $('#'+this.env.form_id)).removeClass('error');
}
this.check_required_fields = function(data)
{
var i, n, is_empty, ret = true,
req_fields = this.env.required_fields;
for (i=0; i<req_fields.length; i++) {
n = req_fields[i];
is_empty = 0;
if ($.isArray(data[n]))
is_empty = (data[n].length == 0) ? 1 : 0;
else
is_empty = !data[n];
if (is_empty) {
this.form_value_error(n);
ret = false;
}
}
return ret;
};
/*********************************************************/
/********* Client commands *********/
/*********************************************************/
this.main_logout = function(params)
{
location.href = '?task=main&action=logout' + (params ? '&' + $.param(params) : '');
return false;
};
this.domain_info = function(id)
{
this.http_post('domain.info', {id: id});
};
this.domain_list = function(props)
{
this.list_handler('domain', props);
};
this.domain_delete = function(id)
{
this.set_busy(true, 'deleting');
this.api_post('domain.delete', {'id': id}, 'domain_delete_response');
};
this.domain_save = function(reload, section)
{
var data = this.serialize_form('#'+this.env.form_id),
action = data.id ? 'edit' : 'add';
if (reload) {
data.section = section;
this.http_post('domain.' + action, {data: data});
return;
}
this.form_error_clear();
if (!this.check_required_fields(data)) {
this.display_message('form.required.empty', 'error');
return;
}
this.set_busy(true, 'saving');
this.api_post('domain.' + action, data, 'domain_' + action + '_response');
};
this.domain_delete_response = function(response)
{
this.response_handler(response, 'domain.delete', 'domain.list', {refresh: 1});
};
this.domain_add_response = function(response)
{
this.response_handler(response, 'domain.add', 'domain.list', {refresh: 1});
};
this.domain_edit_response = function(response)
{
this.response_handler(response, 'domain.edit', 'domain.list', {refresh: 1});
};
this.user_info = function(id)
{
this.http_post('user.info', {'id': id});
};
this.user_list = function(props)
{
this.list_handler('user', props);
};
this.user_delete = function(id)
{
this.set_busy(true, 'deleting');
this.api_post('user.delete', {'id': id}, 'user_delete_response');
};
this.user_save = function(reload, section)
{
var data = this.serialize_form('#'+this.env.form_id),
action = data.id ? 'edit' : 'add';
if (reload) {
data.section = section;
this.http_post('user.' + action, {data: data});
return;
}
this.form_error_clear();
// check password
if (data.userpassword != data.userpassword2) {
this.display_message('user.password.mismatch', 'error');
this.form_value_error('userpassword2');
return;
}
delete data['userpassword2'];
if (!this.check_required_fields(data)) {
this.display_message('form.required.empty', 'error');
return;
}
this.set_busy(true, 'saving');
this.api_post('user.' + action, data, 'user_' + action + '_response');
};
this.user_delete_response = function(response)
{
this.response_handler(response, 'user.delete', 'user.list');
};
this.user_add_response = function(response)
{
this.response_handler(response, 'user.add', 'user.list');
};
this.user_edit_response = function(response)
{
this.response_handler(response, 'user.edit', 'user.list');
};
this.group_info = function(id)
{
this.http_post('group.info', {id: id});
};
this.group_list = function(props)
{
this.list_handler('group', props);
};
this.group_delete = function(id)
{
this.set_busy(true, 'deleting');
this.api_post('group.delete', {'id': id}, 'group_delete_response');
};
this.group_save = function(reload, section)
{
var data = this.serialize_form('#'+this.env.form_id),
action = data.id ? 'edit' : 'add';
if (reload) {
data.section = section;
this.http_post('group.' + action, {data: data});
return;
}
this.form_error_clear();
if (!this.check_required_fields(data)) {
this.display_message('form.required.empty', 'error');
return;
}
this.set_busy(true, 'saving');
this.api_post('group.' + action, data, 'group_' + action + '_response');
};
this.group_delete_response = function(response)
{
this.response_handler(response, 'group.delete', 'group.list');
};
this.group_add_response = function(response)
{
this.response_handler(response, 'group.add', 'group.list');
};
this.group_edit_response = function(response)
{
this.response_handler(response, 'group.edit', 'group.list');
};
this.resource_info = function(id)
{
this.http_post('resource.info', {id: id});
};
this.resource_list = function(props)
{
this.list_handler('resource', props);
};
this.resource_delete = function(id)
{
this.set_busy(true, 'deleting');
this.api_post('resource.delete', {'id': id}, 'resource_delete_response');
};
this.resource_save = function(reload, section)
{
var data = this.serialize_form('#'+this.env.form_id),
action = data.id ? 'edit' : 'add';
if (reload) {
data.section = section;
this.http_post('resource.' + action, {data: data});
return;
}
this.form_error_clear();
if (!this.check_required_fields(data)) {
this.display_message('form.required.empty', 'error');
return;
}
this.set_busy(true, 'saving');
this.api_post('resource.' + action, data, 'resource_' + action + '_response');
};
this.resource_delete_response = function(response)
{
this.response_handler(response, 'resource.delete', 'resource.list');
};
this.resource_add_response = function(response)
{
this.response_handler(response, 'resource.add', 'resource.list');
};
this.resource_edit_response = function(response)
{
this.response_handler(response, 'resource.edit', 'resource.list');
};
this.role_info = function(id)
{
this.http_post('role.info', {id: id});
};
this.role_list = function(props)
{
this.list_handler('role', props);
};
this.role_delete = function(id)
{
this.set_busy(true, 'deleting');
this.api_post('role.delete', {'id': id}, 'role_delete_response');
};
this.role_save = function(reload, section)
{
var data = this.serialize_form('#'+this.env.form_id),
action = data.id ? 'edit' : 'add';
if (reload) {
data.section = section;
this.http_post('role.' + action, {data: data});
return;
}
this.form_error_clear();
if (!this.check_required_fields(data)) {
this.display_message('form.required.empty', 'error');
return;
}
this.set_busy(true, 'saving');
this.api_post('role.' + action, data, 'role_' + action + '_response');
};
this.role_delete_response = function(response)
{
this.response_handler(response, 'role.delete', 'role.list');
};
this.role_add_response = function(response)
{
this.response_handler(response, 'role.add', 'role.list');
};
this.role_edit_response = function(response)
{
this.response_handler(response, 'role.edit', 'role.list');
};
this.sharedfolder_info = function(id)
{
this.http_post('sharedfolder.info', {id: id});
};
this.sharedfolder_list = function(props)
{
this.list_handler('sharedfolder', props);
};
this.sharedfolder_delete = function(id)
{
this.set_busy(true, 'deleting');
this.api_post('sharedfolder.delete', {'id': id}, 'sharedfolder_delete_response');
};
this.sharedfolder_save = function(reload, section)
{
var data = this.serialize_form('#'+this.env.form_id),
action = data.id ? 'edit' : 'add';
if (reload) {
data.section = section;
this.http_post('sharedfolder.' + action, {data: data});
return;
}
this.form_error_clear();
if (!this.check_required_fields(data)) {
this.display_message('form.required.empty', 'error');
return;
}
this.set_busy(true, 'saving');
this.api_post('sharedfolder.' + action, data, 'sharedfolder_' + action + '_response');
};
this.sharedfolder_delete_response = function(response)
{
this.response_handler(response, 'sharedfolder.delete', 'sharedfolder.list');
};
this.sharedfolder_add_response = function(response)
{
this.response_handler(response, 'sharedfolder.add', 'sharedfolder.list');
};
this.sharedfolder_edit_response = function(response)
{
this.response_handler(response, 'sharedfolder.edit', 'sharedfolder.list');
};
this.settings_type_info = function(id)
{
this.http_post('settings.type_info', {id: id});
};
this.settings_type_add = function()
{
this.http_post('settings.type_add', {type: $('#type_list_filter').val()});
};
this.settings_type_list = function(props)
{
if (!props)
props = {};
if (props.search === undefined && this.env.search_request)
props.search_request = this.env.search_request;
props.type = $('#type_list_filter').val();
this.http_post('settings.type_list', props);
};
this.type_delete = function(id)
{
this.set_busy(true, 'deleting');
this.api_post('type.delete', this.type_id_parse(id), 'type_delete_response');
};
this.type_save = function(reload, section)
{
var i, n, attr, request = {},
data = this.serialize_form('#'+this.env.form_id),
action = data.id ? 'edit' : 'add',
required = this.env.attributes_required || [];
if (reload) {
data.section = section;
this.http_post('type.' + action, {data: data});
return;
}
this.form_error_clear();
if (!this.check_required_fields(data)) {
this.display_message('form.required.empty', 'error');
return;
}
if (data.key.match(/[^a-z_-]/)) {
this.display_message('attribute.key.invalid', 'error');
return;
}
request.id = data.id;
request.key = data.key;
request.name = data.name;
request.type = data.type;
request.description = data.description;
request.used_for = data.used_for;
request.attributes = {fields: {}, form_fields: {}, auto_form_fields: {}};
request.attributes.fields.objectclass = data.objectclass;
// Build attributes array compatible with the API format
// @TODO: use attr_table format
for (i in this.env.attr_table) {
// attribute doesn't exist in specified object classes set
if (!(n = this.env.attributes[i]))
continue;
// check required attributes
if (required.length)
required = $.map(required, function(a) { return a != n ? a : null; });
attr = this.env.attr_table[i];
data = {};
if (attr.valtype == 'static') {
request.attributes.fields[i] = attr.data;
continue;
}
if (attr.type == 'list-autocomplete') {
data.type = 'list';
data.autocomplete = true;
}
else if (attr.type != 'text')
data.type = attr.type;
if ((attr.type == 'select' || attr.type == 'multiselect') && attr.values && attr.values.length)
data.values = attr.values;
if (attr.optional)
data.optional = true;
if (attr.maxcount)
data.maxcount = attr.maxcount;
if (attr.validate != 'default')
data.validate = attr.validate;
if (attr['default'] && attr.valtype == 'normal' && attr.type.match(/^(text|select)/))
data['default'] = attr['default'];
if (attr.valtype == 'normal' || attr.valtype == 'auto')
request.attributes.form_fields[i] = data;
if (attr.valtype == 'auto' || attr.valtype == 'auto-readonly') {
if (attr.data)
data.data = attr.data.split(/,/);
request.attributes.auto_form_fields[i] = data;
}
}
if (required.length) {
this.display_message(this.t('attribute.required.error').replace(/\$1/, required.join(',')), 'error');
return;
}
this.set_busy(true, 'saving');
this.api_post('type.' + action, request, 'type_' + action + '_response');
};
this.type_delete_response = function(response)
{
this.response_handler(response, 'type.delete', 'settings.type_list');
};
this.type_add_response = function(response)
{
this.response_handler(response, 'type.add', 'settings.type_list');
};
this.type_edit_response = function(response)
{
this.response_handler(response, 'type.edit', 'settings.type_list');
};
/*********************************************************/
/********* Various helper methods *********/
/*********************************************************/
// universal API response handler
this.response_handler = function(response, action, list, list_params)
{
if (!this.api_response(response))
return;
this.display_message(action + '.success');
var page = this.env.list_page,
list_id = list.replace(/[^a-z]/g, '');
// if objects list exists
if ($('#'+list_id).length) {
// goto previous page if last record on the current page has been deleted
if (this.env.list_page > 1 && this.env.list_size == 1 && action.match(/\.delete/))
page -= 1;
if (!list_params)
list_params = {};
list_params.page = page;
this.command(list, list_params);
this.set_watermark('taskcontent');
}
};
// universal list request handler
this.list_handler = function(type, props)
{
if (!props)
props = {};
if (props.search === undefined && this.env.search_request)
props.search_request = this.env.search_request;
this.http_post(type + '.list', props);
};
// Parses object type identifier
this.type_id_parse = function(id)
{
var id = String(id).split(':');
return {type: id[0], id: id[1]};
};
// Removes attribute row
this.type_attr_delete = function(attr)
{
$('#attr_table_row_' + attr).remove();
$('select[name="attr_name"] > option[value="'+attr+'"]').show();
delete this.env.attr_table[attr];
this.type_attr_cancel();
};
// Displays attribute edition form
this.type_attr_edit = function(attr)
{
var form = $('#type_attr_form');
form.detach();
$('#attr_table_row_'+attr).after(form);
this.type_attr_form_init(attr);
form.slideDown(400);
};
// Displays attribute addition form
this.type_attr_add = function()
{
var form = $('#type_attr_form');
form.detach();
$('#type_attr_table > tbody').append(form);
this.type_attr_form_init();
form.slideDown(400);
};
// Saves attribute form, create/update attribute row
this.type_attr_save = function()
{
var attr, row, data = {},
form_data = this.serialize_form('#'+this.env.form_id),
name_select = $('select[name="attr_name"]');
// read attribute form data
data.type = form_data.attr_type;
data.valtype = form_data.attr_value;
data.optional = form_data.attr_optional;
data.validate = form_data.attr_validate;
data.data = data.valtype != 'normal' ? form_data.attr_data : null;
data.maxcount = data.type == 'list' || data.type == 'list-autocomplete' ? form_data.attr_maxcount : 0;
data.values = data.type == 'select' || data.type == 'multiselect' ? form_data.attr_options : [];
if (name_select.is(':visible')) {
// new attribute
attr = name_select.val();
row = $('<tr><td class="name"></td><td class="type"></td><td class="readonly"></td>'
+'<td class="optional"></td><td class="validate"></td><td class="actions">'
+'<a class="button delete" title="delete" onclick="kadm.type_attr_delete(\''+attr+'\')" href="#delete"></a>'
+'<a class="button edit" title="edit" onclick="kadm.type_attr_edit(\''+attr+'\')" href="#edit"></a></td></tr>')
.attr('id', 'attr_table_row_' + attr).appendTo('#type_attr_table > tbody');
}
else {
// edited attribute
attr = $('span', name_select.parent()).text().toLowerCase();
row = $('#attr_table_row_' + attr);
}
if (data.valtype != 'normal') {
row.attr('title', this.t('attribute.value.' + (data.valtype == 'static' ? 'static' : 'auto')) + ': ' + data.data);
}
if (form_data.attr_default && data.valtype == 'normal' && data.type.match(/^(text|select)/)) {
data['default'] = form_data.attr_default;
}
// Update table row
$('td.name', row).text(this.env.attributes[attr]);
$('td.type', row).text(data.type);
$('td.readonly', row).text(data.valtype == 'auto-readonly' ? this.env.yes_label : this.env.no_label);
$('td.optional', row).text(data.optional ? this.env.yes_label : this.env.no_label);
$('td.validate', row).text(this.t('attribute.validate.' + data.validate));
// Update env data
this.env.attr_table[attr] = data;
this.type_attr_cancel();
};
// Hide attribute form
this.type_attr_cancel = function()
{
$('#type_attr_form').hide();
};
this.type_attr_form_init = function(attr)
{
var name_select = $('select[name="attr_name"]'),
data = attr ? this.env.attr_table[attr] : {},
type = data.type || 'text';
$('select[name="attr_type"]').val(type);
$('select[name="attr_value"]').val(attr ? data.valtype : 'normal');
$('select[name="attr_validate"]').val(attr ? data.validate : '');
$('input[name="attr_default"]').val(data['default'] || '');
$('input[name="attr_optional"]').attr('checked', attr ? data.optional : false);
$('input[name="attr_data"]').val(data.data || '');
$('input[name="attr_maxcount"]').val(data.maxcount || '');
$('textarea[name="attr_options"]').val(data.values ? data.values.join("\n") : '');
$('span', name_select.parent()).remove();
if (attr) {
name_select.hide().val(attr);
$('<span></span>').text(this.env.attributes[attr] ? this.env.attributes[attr] : attr)
.appendTo(name_select.parent());
}
else {
this.type_attr_select_init();
name_select.show();
}
this.form_element_update({name: 'attr_options'});
this.type_attr_type_change();
this.type_attr_value_change();
};
// Initialize attribute name selector
this.type_attr_select_init = function()
{
var select = $('select[name="attr_name"]'),
options = $('option', select);
options.each(function() {
if (kadm.env.attr_table[this.value])
$(this).attr('disabled', true);
});
options.not(':disabled').first().attr('selected', true);
};
// Update attribute form on attribute name change
this.type_attr_name_change = function(elem)
{
this.type_attr_value_change();
};
// Update attribute form on value type change
this.type_attr_value_change = function(elem)
{
var type = $(elem || 'select[name="attr_value"]').val(),
field_type = $('select[name="attr_type"]').val(),
optional = $('#attr_form_row_optional'),
select = $('select[name="attr_name"]').val(),
attr_name = this.env.attributes[select],
// only non-static and non-required attributes can be marked as optional
opt = type != 'static' && $.inArray(attr_name, this.env.attributes_required) == -1;
$('input[name="attr_data"]')[type != 'normal' ? 'show' : 'hide']();
$('#attr_form_row_readonly')[type != 'static' ? 'show' : 'hide']();
$('#attr_form_row_default')[type == 'normal' && field_type.match(/^(text|select)/) ? 'show' : 'hide']();
optional[opt ? 'show' : 'hide']();
if (!opt)
$('input', optional).attr('checked', false);
};
// Update attribute form on type change
this.type_attr_type_change = function(elem)
{
var type = $(elem || 'select[name="attr_type"]').val(),
val_type = $('select[name="attr_value"]').val();
$('#attr_form_row_maxcount')[type == 'list' || type == 'list-autocomplete' ? 'show' : 'hide']();
$('#attr_form_row_options')[type == 'select' || type == 'multiselect' ? 'show' : 'hide']();
$('#attr_form_row_default')[val_type == 'normal' && type.match(/^(text|select)/) ? 'show' : 'hide']();
};
// Update attributes list on object classes change
this.type_attr_class_change = function(field)
{
var data = {attributes: 'attribute', classes: this.type_object_classes(field)};
this.api_post('form_value.select_options', data, 'type_attr_class_change_response');
this.type_attr_cancel();
};
// Update attributes list on object classes change - API response handler
this.type_attr_class_change_response = function(response)
{
if (!this.api_response(response))
return;
var i, lc, list = response.result.attribute.list || [],
required = response.result.attribute.required || [],
select = $('select[name="attr_name"]');
// remove objectClass from attributes list(s)
required = $.map(required, function(a) { return a == 'objectClass' ? null : a; });
list = $.map(list, function(a) { return a == 'objectClass' ? null : a; });
this.env.attributes = {};
this.env.attributes_required = required;
select.empty();
for (i in list) {
lc = list[i].toLowerCase();
this.env.attributes[lc] = list[i];
$('<option>').text(list[i]).val(lc).appendTo(select);
}
};
// Return selected objectclasses array
this.type_object_classes = function(field)
{
var classes = [];
$('option:selected', $(field)).each(function() {
classes.push(this.value);
});
return classes;
};
// Password generation - request
this.generate_password = function(fieldname)
{
this.env.password_field = fieldname;
this.set_busy(true, 'loading');
// we can send only 'attributes' here, because password generation doesn't require object type name/id
this.api_post('form_value.generate', {attributes: [fieldname]}, 'generate_password_response');
};
// Password generation - response handler
this.generate_password_response = function(response)
{
if (!this.api_response(response))
return;
var f = this.env.password_field, pass = response.result[f];
$('input[name="' + f + '"]').val(pass);
$('input[name="' + f + '2"]').val(pass);
};
// LDAP URL parser
this.parse_ldap_url = function(url)
{
var result = {},
url_parser = /^([^:]+):\/\/([^\/]*)\/([^?]*)\?([^?]*)\?([^?]*)\?(.*)$/;
matches = url_parser.exec(url);
if (matches && matches[1])
return {
scheme: matches[1],
host: matches[2],
base: matches[3],
attrs: matches[4],
scope: matches[5],
filter: this.parse_ldap_filter(matches[6])
};
};
// LDAP filter parser
this.parse_ldap_filter = function(filter)
{
var chr, next, elem, pos = 0, join, level = -1, res = [],
len = filter ? filter.length : 0;
if (!filter.match(/^\(.*\)$/))
filter = '(' + filter + ')';
while (len > pos) {
chr = filter.charAt(pos);
if (chr == '&') {
join = 'AND';
level++;
}
else if (chr == '|') {
join = 'OR';
level++;
}
else if (chr == '(') {
next = filter.charAt(pos+1);
if (next != '&' && next != '|') {
next = filter.indexOf(')', pos);
if (next > 0) {
if (elem = this.parse_ldap_filter_entry(filter.substr(pos + 1, next - pos - 1))) {
elem.join = join;
elem.level = level;
res.push(elem);
}
pos = next;
}
}
}
else if (chr == ')')
level--;
pos++;
}
return res;
};
// LDAP filter-entry parser
this.parse_ldap_filter_entry = function(entry)
{
var type = 'exact', name, value;
if (entry.match(/^([a-zA-Z0-9_-]+)=(.*)$/)) {
name = RegExp.$1;
value = RegExp.$2;
if (value == '*') {
value = ''
type = 'any';
}
else if (value.match(/^\*(.+)\*$/)) {
value = RegExp.$1;
type = 'both';
}
else if (value.match(/^\*(.+)$/)) {
value = RegExp.$1;
type = 'suffix';
}
else if (value.match(/^(.+)\*$/)) {
value = RegExp.$1;
type = 'prefix';
}
return {name: name, type: type, value: value};
}
};
// Builds LDAP filter string from the defined structure
this.build_ldap_filter = function(filter)
{
var i, elem, str = '', last = -1, join = {'AND': '&', 'OR': '|'};
for (i=0; i<filter.length; i++) {
elem = filter[i];
if (elem.level > last)
str += (elem.join && filter.length > 1 ? join[elem.join] : '');
else if (elem.level < last)
str += ')';
str += '(' + elem.name + '=';
if (elem.type == 'any')
str += '*';
else if (elem.type == 'both')
str += '*' + elem.value + '*';
else if (elem.type == 'prefix')
str += elem.value + '*';
else if (elem.type == 'suffix')
str += '*' + elem.value;
else
str += elem.value;
str += ')';
last = elem.level;
}
if (filter.length > 1)
str = '(' + str + ')';
return str;
};
// Builds LDAP URL string from the defined structure
this.build_ldap_url = function(params)
{
var url = '';
if (!params.filter.length && !params.base)
return url;
url += (params.scheme ? params.scheme : 'ldap') + '://';
url += (params.host ? params.host + (params.port && params.port != 389 ? ':'+params.port : '') : '') + '/';
url += (params.base ? params.base : '') + '?';
url += (params.attrs ? params.attrs : '') + '?';
url += (params.scope ? params.scope : '') + '?';
if (params.filter)
url += this.build_ldap_filter(params.filter);
return url;
};
};
// Add escape() method to RegExp object
// http://dev.rubyonrails.org/changeset/7271
RegExp.escape = function(str)
{
return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
// make a string URL safe (and compatible with PHP's rawurlencode())
function urlencode(str)
{
if (window.encodeURIComponent)
return encodeURIComponent(str).replace('*', '%2A');
return escape(str)
.replace('+', '%2B')
.replace('*', '%2A')
.replace('/', '%2F')
.replace('@', '%40');
};
// Initialize application object (don't change var name!)
var kadm = new kolab_admin();
// general click handler
$(document).click(function() {
// destroy autocompletion
kadm.ac_stop();
$('.popup').hide();
});
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Apr 4, 7:12 AM (1 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820994
Default Alt Text
(118 KB)
Attached To
Mode
rWAP webadmin
Attached
Detach File
Event Timeline