ACC SHELL

Path : /usr/share/YaST2/modules/
File Upload :
Current File : //usr/share/YaST2/modules/LdapPopup.ycp

/**
 * File:	modules/LdapPopup.ycp
 * Package:	Configuration of LDAP
 * Summary:	Additional user interface functions: special edit popups
 * Authors:	Jiri Suchomel <jsuchome@suse.cz>
 *
 * $Id: LdapPopup.ycp 51747 2008-10-01 13:35:55Z jsuchome $
 *
 * Popups for editing the values of LDAP configuration tables.
 */

{

module "LdapPopup";
textdomain "ldap-client";

import "Ldap";
import "Label";
import "Popup";
import "Wizard";

    /**
     * Popup for browsing LDAP tree and selecting the DN
     * WARNING we expect that LDAP connection is already correctly initialized !
     * @param root_dn the starting point (root of tree); if empty string is
     * given, the search for available bases will be done automatically
     * @return DN of selected item, empty string when canceled
     */
    global define string BrowseTree (string root_dn) {

	// get display mode
	map display_info = UI::GetDisplayInfo();
	boolean textmode = display_info["TextMode"]:true;

	// map of already read subtrees
	map<string,boolean> dns	= $[];
	// selected DN (return value)
	string current_dn	= "";

	term contents = `HBox (`VSpacing (20), `VBox (`HSpacing(70),
	    `VSpacing (0.2),
	    `HBox (
		`HSpacing (),
		`ReplacePoint (`id (`reptree), `Tree (`id(`tree), root_dn, [])),
		`HSpacing ()
	    ),
	    `HBox (
		`HSpacing (1.5),
	        `PushButton (`id (`ok), `opt (`key_F10), Label::OKButton()),
		`PushButton(`id (`cancel),`opt (`key_F9),Label::CancelButton()),
		// pushbutton label
		textmode ?
		  // button label
		  `Right (`PushButton (`id (`open), `opt (`key_F6),_("&Open"))):
		  `Empty (),
		`HSpacing (1.5)
	    ),
	    `VSpacing (0.2)
	));

	UI::OpenDialog (`opt (`decorated), contents);

	list<term> items	= [];
	list<string> out	= (list<string>) SCR::Read (.ldap.search, $[
		"base_dn"	: root_dn,
		"scope"		: root_dn != "" ? 0 : 1,
		"dn_only"	: true,
		"not_found_ok"	: true ]
	);
	if (size (out) > 0)
	{
	    items = maplist (string dn, out, {
		dns [dn]	= false;
		return `item (dn, false, []);
	    });
	}

	if (size (items) > 0)
	{
	    UI::ReplaceWidget (`id (`reptree), textmode ?
		`Tree (`id(`tree), root_dn, items) :
		`Tree (`id(`tree), `opt(`notify), root_dn, items));
	    // no item is selected
	    UI::ChangeWidget (`tree, `CurrentItem, nil);
	}
	else if (root_dn == "")
	{
	    list bases = (list) SCR::Read (.ldap.search, $[
		"base_dn"	: "",
	        "scope"		: 0,
		"attrs"		: [ "namingContexts" ]
	    ]);
	    if (size (bases) > 0)
		items = maplist (string dn, bases[0,"namingContexts"]:[],
		    ``(`item(dn, false, [])));
	    if (size (items) > 0)
	    {
		UI::ReplaceWidget (`id (`reptree), textmode ?
		    `Tree (`id(`tree), root_dn, items) :
		    `Tree (`id(`tree), `opt(`notify), root_dn, items));
		UI::ChangeWidget (`tree, `CurrentItem, nil);
	    }
	}

	if (textmode)
	    UI::SetFocus (`id(`tree));

	list<string> subdns	= [];
	map open_items		= $[];

	define list<term> update_items (list<term> its) {

	    return maplist (term it, its, {
		string dn	= it[0]:"";
		if (dn == current_dn)
		{
		    return `item (dn, true, maplist (string k,
			subdns, ``(`item (k, false, []))));
		}
		integer last = size (it) - 1;
		if (size (it[last]:[]) == 0)
		    return it;
		// `OpenItems doesn't work in ncurses...
		boolean open = haskey (open_items, dn) && !textmode;
		return `item (dn, open, update_items (it[last]:[]));
	    });
	}

	string retval = root_dn;
	while (true)
	{
	    any ret = UI::UserInput ();
	    if (ret == `tree || ret == `open)
	    {
		current_dn = (string) UI::QueryWidget (`id(`tree),`CurrentItem);
		if (! dns[current_dn]:false)
		{
		    subdns = (list<string>) SCR::Read (.ldap.search, $[
			"base_dn"	: current_dn,
			"scope"		: 1,
			"dn_only"	: true,
			"not_found_ok"	: true,
		    ]);
		    dns [current_dn]	= true;
		    if (size (subdns) > 0)
		    {
			open_items = (map) UI::QueryWidget (`tree, `OpenItems);
			items	= update_items (items);
			UI::ReplaceWidget (`id (`reptree), textmode ?
			    `Tree (`id(`tree), root_dn, items) :
			    `Tree (`id(`tree), `opt(`notify), root_dn, items));
			UI::ChangeWidget (`id(`tree), `CurrentItem, current_dn);
			open_items = $[];
		    }
		}
		if (textmode)
		    UI::SetFocus (`id(`tree));
	    }
	    if (ret == `cancel)
	    {
		retval = "";
		break;
	    }
	    if (ret == `ok)
	    {
		string dn = (string)UI::QueryWidget (`id(`tree), `CurrentItem);
		if (dn != nil)
		    retval = dn;
		else retval = current_dn;
		break;
	    }
	}
	UI::CloseDialog ();
	return retval;
    }

    /**
     * Open the LDAP Browse popup and initialize initialize LDAP connection
     * before.
     * @param connection init map with information passed to ldap agent
     * (see ldap agent '.ldap' Execute call documentation)
     * @param root_dn the starting point (root of tree); if empty string is
     * given, the search for available bases will be done automatically
     * @return DN of selected item, empty string when canceled
     */
    global define string InitAndBrowseTree (string root_dn, map connection) {

	map args = size (connection) > 0 ? connection : $[
	    "hostname"	: Ldap::GetFirstServer (Ldap::server),
	    "port"	: Ldap::GetFirstPort (Ldap::server),
	    "version"	: Ldap::ldap_v2 ? 2 : 3,
	    "use_tls"	: Ldap::ldap_tls ? "yes" : "no"
	];
	string error	= Ldap::LDAPInitWithTLSCheck (args);
	if (error != "")
	{
	    Ldap::LDAPErrorMessage ("init", error);
	    return root_dn;
	}
	return BrowseTree (root_dn);
    }


/**
 * Generic popup for editing attribute's value
 * @param map with settings, it could have these keys:
 *  "attr"	attribute name
 *  "value"	current attribute values
 *  "conflicts" list of forbidden values (e.g. existing 'cn' values)
 *  "single"	if attribute can have only one value
 *  "offer"	list of possible values for current attribute (e.g. existing
 *		groups for "secondaryGroup" attribute)
 *  "browse"	if Browse LDAP Tree widget should be provided for choosing DN
 * @return list of atrtibute values (edited or unchanged)
 */
global define list<string> EditAttribute (map settings)
{

    string attr		= settings["attr"]:"";
    list<string> value	= settings["value"]:[];
    list conflicts	= settings["conflicts"]:[];
    list offer		= settings["offer"]:[];
    boolean single	= settings["single"]:false;
    boolean browse	= settings["browse"]:false;

    // help text 1/3
    string help_text = _("<p>Set the new value for the current attribute.</p>") +

    // help text 2/3
    _("<p>If the attribute can have more values, add new entries
with <b>Add Value</b>. Sometimes the button contains the list of
possible values to use for the current attribute.
If the value of the edited attribute should be a distinguished name (DN),
it is possible to choose it from the LDAP tree using <b>Browse</b>.
</p>
");

    string desc = Ldap::AttributeDescription (attr);

    if (desc != "")
	// help text 3/3, %1 is attribute name, description follows.
	// The description will be not translated: maybe add a note
	// "available only in english" to the sentence for other languages?
	// Example:
	// "<p>The description of attribute \"%1\"<br>(available only in english):</p>"
	// or:
	// "<p>The description (only in english) of attribute \"%1\":<br></p>"
	help_text = help_text + sformat (_("<p>The description of attribute \"%1\":<br></p>"), attr) +

	sformat ("<p><i>%1</i></p>", desc);

    list<string> org_value = value;
    integer value_size = size (value);

    // horizontal size of popup for
    integer hsize = size (value[0]:"") + 10;

    /**
     * Helper for creating items for EditAttribute Popup
     */
    define term generate_value_list () ``{
	term ret = `VBox();
	if (single)
	{
	    ret = add (ret, `InputField (`id (0), `opt (`hstretch),
		// textentry label
		sformat(_("&Value of \"%1\" Attribute"), attr), value[0]:""));
	}
	else
	{
	    ret = add (ret, `InputField (`id (0), `opt (`hstretch),
		// textentry label
		sformat(_("&Values of \"%1\" Attribute"), attr), value[0]:""));
	    integer i = 1;
	    while (i < value_size)
	    {
		ret = add (ret, `InputField (`id (i), `opt (`hstretch),
		    "", value[i]:""));
		if (size (value[i]:"" + 10) > hsize)
		    hsize = size (value[i]:"") + 10;
		i = i + 1;
	    }
	}
	return ret;
    }

    term values = generate_value_list ();
    // button label
    term add_button = `PushButton(`id(`new), `opt (`key_F3), _("&Add Value"));

    if (size (offer) > 0 || browse)
    {
	// menubutton item (default value)
	list mb = [ `item(`id(`new), _("&Empty Entry")) ];
	if (browse)
	    mb	= add (mb, `item(`id(`browse), _("Bro&wse")));
	foreach (string it, (list<string>) offer, ``{
	    mb = add (mb, `item(`id(it), it));
	});
	// button label
	add_button = `MenuButton(`id(`mb), `opt (`key_F3), _("&Add Value"), mb);
    }

    UI::OpenDialog( `opt(`decorated), `HBox(
	`HSpacing(1),
	`VBox(
	    `HSpacing (hsize > 50 ? hsize : 50),
	    `ReplacePoint (`id(`rp), values),
	    `HBox(
		`PushButton(`id(`ok),`opt(`default,`key_F10),Label::OKButton()),
		`PushButton(`id(`cancel),`opt (`key_F9), Label::CancelButton()),
		`PushButton(`id(`help),`opt (`key_F2), Label::HelpButton()),
		(single)? `Empty() :
		add_button
	    )
	),
	`HSpacing(1)
    ));
    any result = nil;
    list<string> new_value = [];

    if (value_size == 0)
	value_size = 1;
    UI::SetFocus (`id(value_size-1));
    while (true)
    {
	result = UI::UserInput ();
	if (result == `cancel)
	{
	    new_value = org_value;
	    break;
	}
	if ( result == `help )
	{
	    Wizard::ShowHelp( help_text );
	}
	if (result == `new || contains (offer, result) || result == `browse)
	{
	    integer j = 0;
	    value = [];
	    while (j < value_size)
	    {
		value = add (value, (string) UI::QueryWidget (`id(j), `Value) );
		j = j + 1;
	    }
	    if (contains (offer, result) && value[value_size-1]:"" == "")
	    {
		// relace last empty entry
		value [value_size-1] = (string) result;
	    }
	    else if (result == `browse)
	    {
		value [value_size-1] = BrowseTree ("");
	    }
	    else
	    {
		// add new entry
		value = add (value, (result == `new)? "" : (string) result);
		value_size = value_size + 1;
	    }
	    UI::ReplaceWidget (`id(`rp), generate_value_list ());
	    UI::SetFocus (`id(value_size-1));
	}
	if (result == `ok)
	{
	    integer j = 0;
	    boolean duplicate = false;
	    new_value = [];
	    while (j < value_size && !duplicate)
	    {
		string v = (string) UI::QueryWidget (`id(j), `Value);
		if (!contains (org_value, v) && contains (conflicts, v))
		{
		    //error popup
		    Popup::Error (sformat (_("The value \'%1\' already exists.
Please select another one."), v));
		    duplicate = true;
		}
		if (v != "") new_value = add (new_value, v);
		j = j + 1;
	    }
	    if (duplicate) continue;
	    break;
	}
    }
    UI::CloseDialog();
    return new_value;
}

/**
 * Popup for adding new configuration module
 * @param conflicts list of forbidden names ('cn' values)
 * @param available list of possible object classes for new module
 * @return map of new module (contains its name and object class)
 */
global define map NewModule (list available, list conflicts) ``{

    map descriptions = $[
	// description of configuration object
	"suseuserconfiguration": _("Configuration of user management tools"),
	// description of configuration object
	"susegroupconfiguration": _("Configuration of group management tools"),
    ];
    // label
    term r_buttons = `VBox( `Left(`Label (_("Object Class of New Module"))));
    foreach (string class, (list<string>) available, ``{
	string desc = class;
	if (descriptions [class]:"" != "")
	    desc = sformat ("%1 (%2)", class, descriptions [class]:"");
	r_buttons = add (r_buttons,`Left(`RadioButton (`id(class), desc,true)));
    });
    UI::OpenDialog( `opt(`decorated), `HBox(
	`HSpacing(1),
	`VBox(
	    `HSpacing (50),
	    `RadioButtonGroup (`id(`rb), r_buttons),
	    `InputField (`id (`cn), `opt (`hstretch),
		// textentry label, do not translate "cn"
		_("&Name of New Module (\"cn\" Value)")),
	    `HBox(
		`PushButton(`id(`ok),`opt(`default,`key_F10),Label::OKButton()),
		`PushButton(`id(`cancel),`opt (`key_F9), Label::CancelButton())
	    )
	),
	`HSpacing(1)
    ));
    any result = nil;
    string new_value = "";
    string class = "";

    UI::SetFocus (`id(`cn));
    while (true)
    {
	result = UI::UserInput ();
	if (result == `cancel)
	{
	    new_value = "";
	    break;
	}
	if (result == `ok)
	{
	    new_value = (string) UI::QueryWidget (`id(`cn), `Value);
	    if (contains (conflicts, new_value))
	    {
		//error popup
		Popup::Error (_("The entered value already exists.
Select another one.
"));
		continue;
	    }
	    if (new_value == "")
	    {
		//error popup
		Popup::Error (_("Enter the module name."));
		continue;
	    }
	    class = (string) UI::QueryWidget (`id(`rb), `CurrentButton);
	    break;
	}
    }
    UI::CloseDialog();
    return $[ "class": class, "cn": new_value ];
}

/**
 * Popup for adding new default value (default value is template's attribute)
 * @param conflicts list of attributes already set
 * @param available list of possible attributes
 * @return map of new "default value" (contains attribute name and value)
 */
global define map AddDefaultValue (list available, list conflicts) ``{

    // help text 1/3
    string help_text = _("<p>Here, set the values of attributes belonging
to an object using the current template. Such values are used as defaults when
the new object is created.</p>
") +

    /*
    // help text 2/3 do not translate "defaultObjectClass"
    _("<p>The list of attributes provided in <b>Attribute Name</b> is the
list of allowed attributes for objects described in the \"defaultObjectClass\"
entry of the current template.</p>
") +
    */

    // help text 3/3 do not translate "homedirectory"
    _("<p>You can use special syntax to create attribute
values from existing ones. The expression <i>%attr_name</i> will be replaced
with the value of attribute \"attr_name\" (for example, use \"/home/%uid\"
as a value of \"homeDirectory\").</p>
");

    available = filter (string attr, (list<string>) available, ``(!contains (conflicts,attr)));

    UI::OpenDialog( `opt(`decorated), `HBox(
	`HSpacing(1),
	`VBox(
	    `HSpacing (50),
	    `VSpacing(0.5),
	    // combobox label
	    `Left(`ComboBox (`id (`attr),`opt(`editable), _("Attribute &Name"),
		available)),
	    `VSpacing(0.5),
	    // textentry label
	    `InputField (`id (`val), `opt (`hstretch), _("Attribute &Value")),
	    `VSpacing(0.5),
	    `HBox(
		`PushButton(`id(`ok),`opt(`default,`key_F10),Label::OKButton()),
		`PushButton(`id(`cancel), `opt(`key_F9), Label::CancelButton()),
		`PushButton(`id(`help),`opt (`key_F2), Label::HelpButton())
	    ),
	    `VSpacing(0.5)),
	`HSpacing(1)
    ));
    any result = nil;
    string new_value = "";
    string attr = "";

    UI::SetFocus (`id(`attr));
    while (true)
    {
	result = UI::UserInput ();
	if (result == `cancel)
	{
	    new_value = "";
	    break;
	}
	if ( result == `help )
	{
	    Wizard::ShowHelp( help_text );
	}
	if (result == `ok)
	{
	    attr	= (string) UI::QueryWidget (`id(`attr), `Value);
	    new_value	= (string) UI::QueryWidget (`id(`val), `Value);
	    break;
	}
    }
    UI::CloseDialog();
    return $[ "attr": attr, "value": new_value ];
}

/**
 * dialog for Password Policy configuration object
 * @param ppolicy data with Password Policy object to be edited (as obtained from LDAP search)
 * @return map with modifications of ppolicy object, nil in case of `cancel
 */
global define map PasswordPolicyDialog (map ppolicy) {

    // reduce the list values to single ones
    ppolicy	= mapmap (string a, any val, (map<string,any>)ppolicy, {
	if (is (val, list) && (Ldap::SingleValued (a) || size ((list)val) == 1))
	    val	= ((list)val)[0]:nil;
	if (val == "TRUE" || val == "FALSE")
	    val	= (val == "TRUE");
	return $[ a: val ];
    });
    map ppolicy_orig	= ppolicy;

    // help text for Password Policy Dialog
    string help_text = _("<p>Select the <b>Password Change Policies</b>, <b>Password Aging Policies</b>, and <b>Lockout Policies</b> tabs to choose LDAP password policy groups of attributes to configure.</p>");


    // tab-specific help texts
    map tabs_help_text	= $[
	// help text for pwdInHistory attribute
	`pwchange	: _("<p>Specify the <b>Maximum Number of Passwords Stored in History</b> to set how many previously used passwords should be saved. Saved passwords may not be used.</p>") +

	// help text for pwdMustChange attribute
	_("<p>Check <b>User Must Change Password after Reset</b> to force users to change their passwords after the the password is reset or changed by an administrator.</p>") +

	// help text for pwdAllowUserChange attribute
	_("<p>Check <b>User Can Change Password</b> to allow users to change their passwords.</p>") +

	// help text for pwdSafeModify attribute
	_("<p>If the existing password must be provided along with the new password, check <b>Old Password Required for Password Change</b>.</p>") +

	// help text for pwdCheckQuality attribute
	_("<p>Select whether the password quality should be verified while passwords are modified or added. Select <b>No Checking</b> if passwords should not be checked at all. With <b>Accept Uncheckable Passwords</b>, passwords are accepted even if the check cannot be performed, for example, if the user has provided an encrypted password. With <b>Only Accept Checked Passwords</b> passwords are refused if the quality test fails or the password cannot be checked.</p>") +

	// help text for pwdMinLength attribute
	_("Set the minimum number of characters that must be used in a password in <b>Minimum Password Length</b>.</p>"),

	// help text for pwdMinAge attribute
	`aging		: _("<p><b>Minimum Password Age</b> sets how much time must pass between modifications to the password.</p>") +

	// help text for pwdMaxAge attribute
	_("<p><b>Maximum Password Age</b> sets how long after modification a password expires.</p>") +

	// help text for pwdExpireWarning attribute
	_("<p>In <b>Time before Password Expiration to Issue Warning</b> set how long before a password is due to expire that an expiration warning messages should be given to an authenticating user.</p>") +

	// help text for pwdGraceAuthNLimit attribute
	_("<p>Set the number of times an expired password can be used to authenticate in <b>Allowed Uses of an Expired Password</b>.</p>"),

	// help text for pwdLockout attribute
	`lockout	: _("<p>Check <b>Enable Password Locking</b> to forbid use of a password after a specified number of consecutive failed bind attempts.</p>") +

	// help text for pwdMaxFailure attribute
	_("<p>Set the number of consecutive failed bind  attempts after which the password may not be used to authenticate in <b>Bind Failures to Lock the Password</b>.</p>") +

	// help text for pwdLockoutDuration attribute
	_("<p>Set how long the password cannot be used in <b>Password Lock Duration</b>.</p>") +

	// help text for pwdFailureCountInterval attribute
	_("<p><b>Bind Failures Cache Duration</b> sets how long before password failures are purged from the failure counter even though no successful authentication has occurred.</p>"),
    ];

    // map of attribute names for each tab
    map attributes	= $[
	`pwchange	: [
	    "pwdInHistory", "pwdMustChange", "pwdAllowUserChange",
	    "pwdSafeModify", "pwdCheckQuality", "pwdMinLength"
	],
	`aging		: [
	    "pwdMinAge", "pwdMaxAge", "pwdExpireWarning", "pwdGraceAuthNLimit"
	],
	`lockout	: [
	    "pwdLockout", "pwdLockoutDuration", "pwdMaxFailure",
	    "pwdFailureCountInterval"
	],
    ];

    list time_attributes = [
	"pwdMinAge", "pwdMaxAge", "pwdExpireWarning", "pwdLockoutDuration",
	"pwdFailureCountInterval"
    ];

    map default_values	= $[
	"pwdMustChange"		: false,
	"pwdAllowUserChange"	: true,
	"pwdSafeModify"		: false,
	"pwdLockout"		: false,
    ];

    // maximal value of IntFields
    integer max		= 99999;

    list<term> tabs	= [
	// tab label
	`item(`id(`pwchange), _("&Password Change Policies"), true),
	// tab label
	`item(`id(`aging), _("Pa&ssword Aging Policies")),
	// tab label
	`item(`id(`lockout), _("&Lockout Policies")),
    ];
    term tabs_term = `VBox (
	`DumbTab (`id(`tabs), tabs,
	    `ReplacePoint(`id(`tabContents ), `VBox (`Empty ())))
    );
    boolean has_tabs	= true;
    if (!UI::HasSpecialWidget (`DumbTab))
    {
	has_tabs	= false;
	term tabbar	= `HBox ();
	foreach (term it, tabs, {
	    string label = it[1]:"";
	    tabbar = add (tabbar,`PushButton (it[0]:`id(label), label));
	});
	tabs_term = `VBox (`Left(tabbar),
	    `Frame ("", `ReplacePoint(`id(`tabContents), `Empty ()))
	);
    }

    term contents = tabs_term;

    // generate the term of password policy tab and update the help text
    void set_password_policies_term () {
	integer	pwdcheckquality	= tointeger (ppolicy["pwdCheckQuality"]:"0");
	term tab_cont	= `Top (`HBox (`HSpacing (0.5), `VBox (
	    `VSpacing (0.8),
	    `IntField (`id ("pwdInHistory"),
		// IntField label
		_("Ma&ximum Number of Passwords Stored in History"),
		0, max, tointeger (ppolicy["pwdInHistory"]:"0")),
	    `VSpacing (0.4),
	    `Left (`CheckBox (`id ("pwdMustChange"),
		// checkbox label
		_("U&ser Must Change Password after Reset"),
		ppolicy["pwdMustChange"]:true)),
	    `VSpacing (0.2),
	    `Left (`CheckBox (`id ("pwdAllowUserChange"),
		// checkbox label
		_("&User Can Change Password"),
		ppolicy["pwdAllowUserChange"]:true)),
	    `VSpacing (0.2),
	    `Left (`CheckBox (`id ("pwdSafeModify"),
		// checkbox label
		_("&Old Password Required for Password Change"),
		ppolicy["pwdSafeModify"]:false)),
	    `VSpacing (0.4),
	    // frame label
	    `HBox (`HSpacing (2), `Frame (_("Password Quality Checking"), `VBox(
		`VSpacing (0.5),
		`RadioButtonGroup (`id("pwdCheckQuality"), `VBox (
		    `Left (`RadioButton (`id(0), `opt (`notify),
			_("&No Checking"), pwdcheckquality == 0)),
		    `Left (`RadioButton(`id(1), `opt (`notify),
			_("Acc&ept Uncheckable Passwords"),
			pwdcheckquality == 1)),
		    `Left (`RadioButton(`id(2), `opt (`notify),
			_("&Only Accept Checked Passwords"),
			pwdcheckquality == 2))
		)),
		`VSpacing (0.4),
		// IntField label
		`IntField (`id ("pwdMinLength"), _("&Minimum Password Length"),
		    0, max, tointeger (ppolicy["pwdMinLength"]:"0"))
	    )))
	), `HSpacing (0.5)));

	UI::ReplaceWidget (`tabContents, tab_cont);
	UI::ChangeWidget (`id ("pwdMinLength"), `Enabled, pwdcheckquality > 0);
	return;
    }

    term time_dialog (string id, string label) {

	integer value	= tointeger (ppolicy[id]:"0");
	integer days	= value / (24*60*60);
	if (days > 0) value	= value - (days * 24*60*60);
	integer	hours	= value / (60*60);
	if (hours > 0) value	= value - (hours * 60*60);
	integer minutes	= value / 60;
	if (minutes > 0) value	= value - (minutes * 60);
	return `HBox (`HSpacing (0.3), `Frame (label, `HBox (
	    `IntField (`id (id + "d"), _("Days"), 0, max, days),
	    `IntField (`id (id + "h"), _("Hours"), 0, 23, hours),
	    `IntField (`id (id + "m"), _("Minutes"), 0, 59, minutes),
	    `IntField (`id (id + "s"), _("Seconds"), 0, 59, value)
	)), `HSpacing (0.3));
    }

    integer get_seconds_value (string attr) {

	integer days	= (integer) UI::QueryWidget (`id (attr + "d"), `Value);
	integer hours	= (integer) UI::QueryWidget (`id (attr + "h"), `Value);
	integer minutes	= (integer) UI::QueryWidget (`id (attr + "m"), `Value);
	integer seconds	= (integer) UI::QueryWidget (`id (attr + "s"), `Value);
	return (days * 24*60*60) + (hours * 60*60) + (minutes *60) + seconds;
    }

    // generate the term of password aging tab
    void set_aging_policies_term () {

	term tab_cont = `Top (`HBox (`HSpacing (0.5), `VBox (
	    `VSpacing (0.7),
	    // frame label
	    time_dialog ("pwdMinAge", _("Minimum Password Age")),
	    `VSpacing (0.4),
	    // frame label
	    time_dialog ("pwdMaxAge", _("Maximum Password Age")),
	    `VSpacing (0.4),
	    time_dialog ("pwdExpireWarning",
		// frame label
		_("Time before Password Expiration to Issue Warning")),
	    `VSpacing (0.2),
	    `IntField (`id ("pwdGraceAuthNLimit"),
		// IntField label
		_("Allowed Uses of an Expired Password"), 0, max,
		tointeger (ppolicy["pwdGraceAuthNLimit"]:"0")
	    )
	), `HSpacing (0.5)));
	UI::ReplaceWidget (`tabContents, tab_cont);
	return;
    }

    // generate the term of lockout aging tab
    void set_lockout_policies_term () {

	boolean pwdlockout	= ppolicy["pwdLockout"]:false;

	term tab_cont = `Top (`HBox (`HSpacing (0.5), `VBox (
	    `VSpacing (0.8),
	    `Left (`CheckBox (`id ("pwdLockout"), `opt (`notify),
		// check box label
		_("Enable Password Locking"),
		pwdlockout)),
	    `VSpacing (0.4),
	    `IntField (`id ("pwdMaxFailure"),
		// intField label
		_("Bind Failures to Lock the Password"),
		0, max, tointeger (ppolicy["pwdMaxFailure"]:"0")),
	    // frame label
	    time_dialog ("pwdLockoutDuration", _("Password Lock Duration")),
	    `VSpacing (0.4),
	    time_dialog ("pwdFailureCountInterval",
		// frame label
		_("Bind Failures Cache Duration"))
	), `HSpacing (0.5)));

	UI::ReplaceWidget (`tabContents, tab_cont);
	UI::ChangeWidget (`id ("pwdMaxFailure"), `Enabled, pwdlockout);
	foreach (string suffix, [ "d", "h", "m", "s" ], {
	    UI::ChangeWidget (`id ("pwdLockoutDuration" + suffix),
		`Enabled, pwdlockout);
	    UI::ChangeWidget (`id ("pwdFailureCountInterval" + suffix),
		`Enabled, pwdlockout);
	});
	return;
    }

    symbol current_tab	= `pwchange;
    any result		= nil;

    Wizard::OpenNextBackDialog ();

    // dialog label
    Wizard::SetContentsButtons (_("Password Policy Configuration"), contents,
	help_text + tabs_help_text[current_tab]:"",
	Label::CancelButton(), Label::OKButton());
    Wizard::HideAbortButton();

    set_password_policies_term ();

    while (true)
    {
	result		= UI::UserInput ();

	if (is(result,symbol) &&
	    contains ([`back, `cancel, `abort], (symbol)result))
	    break;

	// save the values from UI
	foreach (string attr, attributes[current_tab]:[], {
	    if (contains (time_attributes, attr))
	    {
		ppolicy[attr]	= sformat ("%1", get_seconds_value (attr));
		return;
	    }
	    any val	= UI::QueryWidget (`id (attr), `Value);
	    if (is (val, integer))
		val	= sformat ("%1", val);
	    ppolicy[attr]	= val;
	});

	if ((result == `pwchange || result == `aging || result == `lockout) &&
	    result!= current_tab)
	{
	    if (result == `pwchange)
		set_password_policies_term ();
	    else if (result == `aging)
		set_aging_policies_term ();
	    else if (result == `lockout)
		set_lockout_policies_term ();
	    current_tab	= (symbol) result;
	    if (has_tabs)
		UI::ChangeWidget (`id (`tabs), `CurrentItem, current_tab);
	    Wizard::SetHelpText (help_text + tabs_help_text[current_tab]:"");
	    continue;
	}
	if (result == `next)
	{
	    boolean cont = false;

	    // check the template required attributes...
	    foreach (string oc, ppolicy["objectClass"]:[], ``{
		if (cont) return;
		foreach (string attr, Ldap::GetRequiredAttributes (oc), ``{
		    any val = ppolicy[attr]:nil;
		    if (!cont && val == nil || val == [] || val == "") {
			//error popup, %1 is attribute name
			Popup::Error (sformat (_("The \"%1\" attribute is mandatory.
Enter a value."), attr));
			UI::SetFocus (`id(`table));
			cont = true;
		    }
		});
	    });
	    if (cont) continue;
	    break;
	}
	// now solve events inside the tabs
	if (current_tab == `pwchange && is (result, integer))
	{
	    UI::ChangeWidget (`id ("pwdMinLength"), `Enabled, result != 0);
	}
	if (current_tab == `lockout && result == "pwdLockout")
	{
	    boolean pwdlockout = (boolean) UI::QueryWidget (`id ("pwdLockout"), `Value);
	    UI::ChangeWidget (`id ("pwdMaxFailure"), `Enabled, pwdlockout);
	    foreach (string suffix, [ "d", "h", "m", "s" ], {
		UI::ChangeWidget (`id ("pwdFailureCountInterval" + suffix),
		    `Enabled, pwdlockout);
		UI::ChangeWidget (`id ("pwdLockoutDuration" + suffix),
		    `Enabled, pwdlockout);
	    });
	}
    }
    Wizard::CloseDialog ();

    map<string,any> ret	= $[];
    if (result == `next)
    {
	foreach (string key, any val, (map<string,any>) ppolicy, {
	    if (!haskey (ppolicy_orig, key) &&
		(val == default_values[key]:nil || val == "0"))
		return;
	    if (val != ppolicy_orig[key]:nil)
	    {
		if (is (val, boolean))
		    val	= (val == true) ? "TRUE" : "FALSE";
		ret[key]	= val;
	    }
	});
    }
    return (result == `next) ? ret : nil;
}

}//EOF

ACC SHELL 2018