/* This file is part of GQ
 *
 * AUTHORS
 *     Sven Herzberg  <herzi@gnome-de.org>
 *
 * Copyright (C) 1998-2003 Bert Vermeulen
 * Copyright (C) 2002-2003 Peter Stamfest
 * Copyright (C) 2006      Sven Herzberg
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#include "gq-server.h"

#ifdef __CYGWIN__
#undef LDAP_OPT_NETWORK_TIMEOUT
#endif

#include <errno.h>
#include <string.h>

#include "configfile.h"
#include "encode.h"
#include "errorchain.h"
#include "gq-enumerations.h"
#include "gq-keyring.h"
#include "gq-login-dialog.h"
#include "gq-sasl.h"
#include "gq-server-list.h"

#include <glib/gi18n.h>
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

struct GqServerPrivate {
	gchar* name;
	gchar* host;
	gint   port;

	gboolean use_tls : 1;
	gboolean hide_internals : 1;

	/* move them into a connection */
	gchar* bind_dn;
	GqBindType bind_type;

	gchar* search_attribute;

	gboolean  ask_pw : 1;
	gboolean  cache_connection : 1;
#warning "FIXME: remove connected"
	gboolean  connected : 1;
	LDAP    * connection;
};
#define P(i) (G_TYPE_INSTANCE_GET_PRIVATE((i), GQ_TYPE_SERVER, struct GqServerPrivate))

#define TRY_VERSION3 1

GqServer*
gq_server_new(gchar const* name)
{
	GqServer* self = NULL;
	gchar* nname = NULL;

	if(!name) {
		static gint untitled = 0;
		do {
			g_free(nname);
			nname = g_strdup_printf(_("Untitled Server %d"), ++untitled);
			self = gq_server_list_get_by_name(gq_server_list_get(), nname);
		} while(self != NULL);
		name = nname;
	}

	self = g_object_new(GQ_TYPE_SERVER,
			    "name", name,
			    NULL);

	g_free(nname);

	return self;
}

void
gq_server_copy(GqServer const* self,
	       GqServer      * target)
{
	copy_ldapserver(target, self);
}

gboolean
gq_server_get_ask_pw(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), FALSE);

	return P(self)->ask_pw;
}

void
gq_server_set_ask_pw(GqServer* self,
		      gboolean  ask_pw)
{
	g_return_if_fail(GQ_IS_SERVER(self));

	if(P(self)->ask_pw == ask_pw) {
		return;
	}

	P(self)->ask_pw = ask_pw;

	g_object_notify(G_OBJECT(self), "ask-pw");
}

gchar const*
gq_server_get_bind_dn(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), NULL);

	return P(self)->bind_dn;
}

void
gq_server_set_bind_dn(GqServer   * self,
		      gchar const* bind_dn)
{
	g_return_if_fail(GQ_IS_SERVER(self));

	if(P(self)->bind_dn == bind_dn ||
	   (P(self)->bind_dn && bind_dn && !strcmp(P(self)->bind_dn, bind_dn)))
	{
		return;
	}

	g_free(P(self)->bind_dn);
	P(self)->bind_dn = g_strdup(bind_dn ? bind_dn : "");

	g_object_notify(G_OBJECT(self), "bind-dn");
}

GqBindType
gq_server_get_bind_type(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), GQ_BIND_SIMPLE);

	return P(self)->bind_type;
}

void
gq_server_set_bind_type(GqServer  * self,
			GqBindType  bind_type)
{
	g_return_if_fail(GQ_IS_SERVER(self));

	if(P(self)->bind_type == bind_type) {
		return;
	}

	P(self)->bind_type = bind_type;

	g_object_notify(G_OBJECT(self), "bind-type");
}

gboolean
gq_server_get_cache_connection(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), FALSE);

	return P(self)->cache_connection;
}

void
gq_server_set_cache_connection(GqServer* self,
		      gboolean  cache_connection)
{
	g_return_if_fail(GQ_IS_SERVER(self));

	if(P(self)->cache_connection == cache_connection) {
		return;
	}

	P(self)->cache_connection = cache_connection;

	g_object_notify(G_OBJECT(self), "cache-connection");
}

gboolean
gq_server_is_connected(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), FALSE);

	return P(self)->connected;
}

void
gq_server_set_connected(GqServer* self,
			gboolean  is_connected)
{
	g_return_if_fail(GQ_IS_SERVER(self));
	g_return_if_fail(is_connected == FALSE || is_connected == TRUE);

	if(P(self)->connected == is_connected) {
		return;
	}

	P(self)->connected = is_connected;

	g_object_notify(G_OBJECT(self), "connected");
}

LDAP*
gq_server_get_connection(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), NULL);

	return P(self)->connection;
}

static int
do_ldap_auth(LDAP    * ld,
	     GqServer* server,
	     int       open_context)
{
	gboolean cancelled;
	int rc = LDAP_SUCCESS;

	// FIXME: rewrite in a way that can keep the dialog open until we're connected
	do {
		gchar const* bind_dn = gq_server_get_bind_dn(server);
		gchar      * bind_pw = gq_keyring_get_password(server);
		gchar      * bind_dn_enc = NULL;
		gchar      * bind_pw_enc = NULL;
		gboolean     new_pw = FALSE;

		cancelled = FALSE;

		if(gq_server_get_ask_pw(server) && !bind_pw) {
			GtkWidget* dialog = gq_login_dialog_new(server);
			gchar const* entered_pw;

			switch(gtk_dialog_run(GTK_DIALOG(dialog))) {
			case GTK_RESPONSE_OK:
				entered_pw = gq_login_dialog_get_password(GQ_LOGIN_DIALOG(dialog));
				if(!bind_pw || (entered_pw && strcmp(bind_pw, entered_pw))) {
					new_pw = TRUE;
					g_free(bind_pw);
					bind_pw = g_strdup(entered_pw);
				}
				break;
			case GTK_RESPONSE_HELP:
				// FIXME: add documentation and something about the login dialog
				break;
			default:
				cancelled = TRUE;
				rc = LDAP_OTHER;
				break;
			}

			gtk_widget_destroy(dialog);
		}

		if(bind_dn) {
			bind_dn_enc = encoded_string(bind_dn);
		}

		if(bind_pw) {
			bind_pw_enc = encoded_string(bind_pw);
		}

		if(!cancelled) {
#warning "FIXME: move into GqServer as far as possible"
			switch(gq_server_get_bind_type(server)) {
				case GQ_BIND_KERBEROS:
#ifdef HAVE_KERBEROS
					rc = ldap_bind_s(ld, bind_dn, bindpw, LDAP_AUTH_KRBV4);
#else
					error_push(open_context,
						_("Cannot use Kerberos bind with '%s'.\n"
						"GQ was compiled without Kerberos support.\n"
						"Run 'configure --help' for more information\n"),
						gq_server_get_name(server));
					statusbar_msg_clear();
					/* XXX - should merge kerberos into sasl (gssapi) */
					rc = LDAP_AUTH_METHOD_NOT_SUPPORTED;
#endif
					break;
				case GQ_BIND_SASL:
					if(gq_sasl_is_available()) {
						rc = gq_ldap_sasl_bind_s(server, ld, bind_pw);
					}
					else
					{
						error_push(open_context,
							_("Cannot use SASL bind with '%s'.\n"
							"GQ was compiled without SASL support.\n"
							"Run 'configure --help' for more information\n"),
							gq_server_get_name(server));
						statusbar_msg_clear();
						rc = LDAP_AUTH_METHOD_NOT_SUPPORTED;
					}
					break;
				default:
					rc = ldap_simple_bind_s(ld, bind_dn_enc, bind_pw_enc);
					break;
			}
		}

		if(rc == LDAP_SUCCESS && new_pw) {
			gq_keyring_save_password(server, bind_pw);
		}

		g_free(bind_dn_enc);
		gq_keyring_free_password(bind_pw_enc);
		gq_keyring_free_password(bind_pw);
	} while(rc != LDAP_SUCCESS && rc != LDAP_AUTH_METHOD_NOT_SUPPORTED && gq_server_get_ask_pw(server) && !cancelled);

	return rc;
}

static int do_ldap_connect(LDAP **ld_out, GqServer *server,
			   int open_context, int flags)
{
     LDAP *ld = NULL;
     int rc = LDAP_SUCCESS;
     int i;
#ifdef LDAP_OPT_NETWORK_TIMEOUT
     struct timeval nettimeout;
#endif

     *ld_out = NULL;

     if (g_utf8_strchr(gq_server_get_host(server), -1, ':') != NULL) {
#ifdef HAVE_LDAP_INITIALIZE
	  rc = ldap_initialize(&ld, gq_server_get_host(server));
	  if (rc != LDAP_SUCCESS) {
	       ld = NULL;
	  }

	  if(!ld) {
	       error_push(open_context, 
			  _("Failed to initialize LDAP structure for server '%1$s': %2$s."),
			  gq_server_get_name(server),
			  ldap_err2string(rc));
	  }
#else
	  ld = NULL;
	  error_push(open_context,
		     _("Cannot connect to '%s'. No URI support available. Your LDAP toolkit does not support LDAP URIs."),
		     server->name);
#endif
     } else {
	  ld = ldap_init(gq_server_get_host(server), gq_server_get_port(server)); 
	  if(!ld) {
	       error_push(open_context, 
			  _("Failed to initialize LDAP structure for server %1$s: %2$s."),
			  gq_server_get_name(server),
			  strerror(errno));
	  }
     }

     if (ld) {
	  server->server_down = 0;
	  server->version = LDAP_VERSION2;

	  /* setup timeouts */
	  i = DEFAULT_LDAP_TIMEOUT;
	  ldap_set_option(ld, LDAP_OPT_TIMELIMIT, &i);
	  
#ifdef LDAP_OPT_NETWORK_TIMEOUT
	  nettimeout.tv_sec = DEFAULT_NETWORK_TIMEOUT;
	  nettimeout.tv_usec = 0;
	  ldap_set_option(ld, LDAP_OPT_NETWORK_TIMEOUT, &nettimeout);
#endif

#ifndef HAVE_OPENLDAP12
	  if (flags & TRY_VERSION3) {
	       /* try to use LDAP Version 3 */
	       
	       int version = LDAP_VERSION3;
	       if (ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION,
				   &version) == LDAP_OPT_SUCCESS) {
		    server->version = LDAP_VERSION3;
/*  	       } else { */
/*  		    error_push(open_context, message); */
/*  		    push_ldap_addl_error(ld, open_context); */
	       }
	  }
#endif

	  if(gq_server_get_use_tls(server)) {
#if defined(HAVE_TLS)
	       {
		    if (server->version != LDAP_VERSION3) {
			 error_push(open_context,
				    _("Server '%s': Couldn't set protocol version to LDAPv3."),
				    gq_server_get_name(server));
		    }
	       }

	       /* Let's turn on TLS */
	       rc = ldap_start_tls_s(ld, NULL, NULL);
	       if(rc != LDAP_SUCCESS) {
		    error_push(open_context,
			       _("Couldn't enable TLS on the LDAP connection to '%1$s': %2$s"),
			       gq_server_get_name(server),
			       ldap_err2string(rc));
		    push_ldap_addl_error(ld, open_context);
		    ldap_unbind(ld);

		    return rc;
	       }
#else
	       error_push(open_context,
			  _("Cannot use TLS with server '%s'. TLS was not found to be supported by your LDAP libraries.\n"
			    "See README.TLS for more information.\n"),
			  gq_server_get_name(server));
#endif /* defined(HAVE_TLS) */
	  }

	  if(server->local_cache_timeout >= 0) {
#if HAVE_LDAP_CLIENT_CACHE
	       ldap_enable_cache(ld, server->local_cache_timeout, 
				 DEFAULT_LOCAL_CACHE_SIZE);
#else
	       error_push(open_context, 
			  _("Cannot cache LDAP objects for server '%s'.\n"
			    "OpenLDAP client-side caching was not enabled.\n"
			    "See configure --help for more information.\n"),
			  gq_server_get_name(server));
#endif
	  }
	  
	  /* perform the auth */
	  rc = do_ldap_auth(ld, server, open_context);

	  if (rc != LDAP_SUCCESS) {
	       /* Maybe we cannot use LDAPv3 ... try again */
	       error_push(open_context,
			  _("Couldn't bind LDAP connection to '%1$s': %2$s"),
			  gq_server_get_name(server), ldap_err2string(rc));
	       push_ldap_addl_error(ld, open_context);
	       statusbar_msg_clear();
	       /* might as well clean this up */
	       ldap_unbind(ld);
	       ld = NULL;
	  } else {
	       /* always store connection handle, regardless of connection
		  caching -- call close_connection() after each operation
		  to do the caching thing or not */
	       P(server)->connection = ld;
	       server->missing_closes = 1;
	       server->incarnation++;
	       gq_server_set_connected(server, TRUE);
	  }

     }

     /* experimental referral stuff ... */
     if (ld) {
	  rc = ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF);

	  if (rc != LDAP_OPT_SUCCESS ) {
	       error_push(open_context,
			  _("Could not disable following referrals on connection to server '%s'."), gq_server_get_name(server));
	       push_ldap_addl_error(ld, open_context);

	       /* this is not a fatal error, so continue */
	  }
     }

     *ld_out = ld;
     return rc;
}

/*
 * open connection to LDAP server, and store connection for caching
 */
static LDAP*
open_connection_ex(int open_context, GqServer *server, int *ldap_errno)
{
     LDAP *ld;
     int rc;

     if (ldap_errno) *ldap_errno = LDAP_SUCCESS;
     if(!server) return NULL;

     server->missing_closes++;

     /* reuse previous connection if available */
     if(P(server)->connection) {
	  if (server->server_down == 0)
	       return gq_server_get_connection(server);
	  else {
	       /* do not leak file descriptors in case we need to
                * "rebind" */
	       ldap_unbind(P(server)->connection);
	       P(server)->connection = NULL;
	       gq_server_set_connected(server, FALSE);
	  }
     }

     if(gq_server_get_port(server) == 389) {
	  statusbar_msg(_("Connecting to %s"), gq_server_get_host(server));
     } else {
	  statusbar_msg(_("Connecting to %1$s port %2$d"),
			gq_server_get_host(server), gq_server_get_port(server));
     }

     ld = NULL;
/*      open_context = error_new_context(_("Error connecting to server")); */

     rc = do_ldap_connect(&ld, server, open_context,
			  TRY_VERSION3);
     if (rc == LDAP_PROTOCOL_ERROR) {
	  /* this might be an indication that the server does not
	     understand LDAP_VERSION3 - retry without trying VERSION3 */
	  rc = do_ldap_connect(&ld, server, open_context, 0);
	  if (rc == LDAP_SUCCESS) error_clear(open_context);
     } else if (rc == LDAP_INVALID_CREDENTIALS) {
	  /* Should the server configuration pop up? */
     }

     /* this is not quite OK: FIXME */
     if (server->quiet) error_clear(open_context);
/*      error_flush(open_context); */

     if (ldap_errno) *ldap_errno = rc;

	if(ld) {
		gq_server_set_connected(server, TRUE);
	}

     return(ld);
}

LDAP*
open_connection(int open_context, GqServer *server)
{
	return open_connection_ex(open_context, server, NULL);
}

/*
 * called after every set of LDAP operations. This preserves the connection
 * if caching is enabled for the server. Set always to TRUE if you want to
 * close the connection regardless of caching.
 */
void
close_connection(GqServer* server,
		 gboolean  always)
{
	g_return_if_fail(GQ_IS_SERVER(server));
	server->missing_closes--;

	if(P(server)->connection &&
	   ((server->missing_closes == 0 && !gq_server_get_cache_connection(server)) || always))
	{
		/* definitely close this connection */
		ldap_unbind(P(server)->connection);
		P(server)->connection = NULL;
		gq_server_set_connected(server, FALSE);
	}
}

/** Returns a ldapserver object (either an existing or a newly
    created) usable to search below the base_url. 

    The server gets looked up in the following way:

    1) the credentials of the parent server get used with a
       newly created ldapserver

    2) The base_url gets looked up as the canonical name. If a match
       is found and the credentials for this server work a copy of the 
       thus found object gets returned

    3) An anonymous bind gets attempted.
  */
GqServer*
get_referral_server(int error_context,
		    GqServer *parent,
		    const char *base_url)
{
     LDAPURLDesc *desc = NULL;
     GqServer *newserver = NULL, *s;
     int ld_err;

     g_assert(parent);

     if (ldap_url_parse(base_url, &desc) == 0) {
	  GString *new_uri = g_string_sized_new(strlen(base_url));
	  g_string_sprintf(new_uri, "%s://%s:%d/",
			   desc->lud_scheme,
			   desc->lud_host,
			   desc->lud_port);

	  newserver = gq_server_new(NULL); // is just a referral server

	  if (! config->never_leak_credentials) {
	       copy_ldapserver(newserver, parent);

	       gq_server_set_name(newserver, new_uri->str);
	       gq_server_set_host(newserver, new_uri->str);
	       g_free_and_dup(newserver->basedn,   desc->lud_dn);

	       /* some sensible settings for the "usual" case:
		  Anonymous bind. Also show referrals */
	       gq_server_set_ask_pw(newserver, FALSE);
	       newserver->show_ref = 1;
	       newserver->quiet    = 1;

	       if (open_connection_ex(error_context,
				      newserver, &ld_err)) {
		    close_connection(newserver, FALSE);

		    statusbar_msg(_("Initialized temporary server-definition '%1$s' from existing server '%2$s'"),
				  new_uri->str, gq_server_get_name(parent));

		    goto done;
	       }
	       if (ld_err == LDAP_SERVER_DOWN) {
		    goto done;
	       }

	       reset_ldapserver(newserver);

	       /* check: do we have this server around already??? */
	       s = server_by_canon_name(new_uri->str, TRUE);

	       if (s) {
		    copy_ldapserver(newserver, s);

		    gq_server_set_name(newserver, new_uri->str);
		    gq_server_set_host(newserver, new_uri->str);
		    g_free_and_dup(newserver->basedn,   desc->lud_dn);

		    /* some sensible settings for the "usual" case:
		       Anonymous bind. Also show referrals */
		    gq_server_set_ask_pw(newserver, FALSE);
		    newserver->show_ref = 1;
		    newserver->quiet    = 1;

		    if (open_connection_ex(error_context,
					   newserver, &ld_err)) {
			 close_connection(newserver, FALSE);
			 statusbar_msg(_("Initialized temporary server-definition '%1$s' from existing server '%2$s'"),
				       new_uri->str, gq_server_get_name(s));
			 goto done;
		    }
		    if (ld_err == LDAP_SERVER_DOWN) {
			 goto done;
		    }
	       }
	  }

	  reset_ldapserver(newserver);

	  /* anonymous */
	  copy_ldapserver(newserver, parent);

	  gq_server_set_name(newserver, new_uri->str);
	  gq_server_set_host(newserver, new_uri->str);
	  g_free_and_dup(newserver->basedn,   desc->lud_dn);

	  if (open_connection_ex(error_context, newserver, &ld_err)) {
	       close_connection(newserver, FALSE);
	       statusbar_msg(_("Initialized temporary server-definition '%1$s' from existing server '%2$s'"),
			     new_uri->str, gq_server_get_name(parent));
	       goto done;
	  }
	  if (ld_err == LDAP_SERVER_DOWN) {
	       goto done;
	  }
     }

 done:
     if (desc) ldap_free_urldesc(desc);
     if (newserver) {
	  newserver->quiet = 0;
	  canonicalize_ldapserver(newserver);
     }

     return newserver;
}

gchar const*
gq_server_get_host(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), NULL);

	return P(self)->host;
}

void
gq_server_set_host(GqServer   * self,
		   gchar const* host)
{
	g_return_if_fail(GQ_IS_SERVER(self));
	g_return_if_fail(host && *host);

	if(host == P(self)->host || (P(self)->host && !strcmp(host, P(self)->host))) {
		// same host
		return;
	}

	g_free(P(self)->host);
	P(self)->host = g_strdup(host);

	g_object_notify(G_OBJECT(self), "host");
}

gchar const*
gq_server_get_name(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), NULL);

	return P(self)->name;
}

void
gq_server_set_name(GqServer   * self,
		   gchar const* name)
{
	g_return_if_fail(GQ_IS_SERVER(self));
	g_return_if_fail(name && *name);

	if(name == P(self)->name || (P(self)->name && !strcmp(name, P(self)->name))) {
		// same name
		return;
	}

	g_free(P(self)->name);
	P(self)->name = g_strdup(name);

	g_object_notify(G_OBJECT(self), "name");
}

gint
gq_server_get_port(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), 389);

	return P(self)->port;
}

void
gq_server_set_port(GqServer* self,
		   gint      port)
{
	g_return_if_fail(GQ_IS_SERVER(self));
	g_return_if_fail(0 < port);

	if(port == P(self)->port) {
		return;
	}

	P(self)->port = port;

	g_object_notify(G_OBJECT(self), "port");
}

gboolean
gq_server_get_hide_internals(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), FALSE);

	return P(self)->hide_internals;
}

void
gq_server_set_hide_internals(GqServer* self,
			     gboolean  hide_internals)
{
	g_return_if_fail(GQ_IS_SERVER(self));

	if(P(self)->hide_internals == hide_internals) {
		return;
	}

	P(self)->hide_internals = hide_internals;

	g_object_notify(G_OBJECT(self), "hide-internals");
}

gchar const*
gq_server_get_search_attribute(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), NULL);

	return P(self)->search_attribute;
}

void
gq_server_set_search_attribute(GqServer   * self,
			       gchar const* search_attribute)
{
	g_return_if_fail(GQ_IS_SERVER(self));

	if(P(self)->search_attribute == search_attribute ||
	   (P(self)->search_attribute && search_attribute &&
	    !strcmp(P(self)->search_attribute, search_attribute)))
	{
		return;
	}

	g_free(P(self)->search_attribute);
	P(self)->search_attribute = g_strdup(search_attribute);

	g_object_notify(G_OBJECT(self), "search-attribute");
}

gboolean
gq_server_get_use_tls(GqServer const* self)
{
	g_return_val_if_fail(GQ_IS_SERVER(self), FALSE);

	return P(self)->use_tls;
}

void
gq_server_set_use_tls(GqServer* self,
		      gboolean  use_tls)
{
	g_return_if_fail(GQ_IS_SERVER(self));

	if(P(self)->use_tls == use_tls) {
		return;
	}

	P(self)->use_tls = use_tls;

	g_object_notify(G_OBJECT(self), "use-tls");
}

/* saves typing */
#define DEEPCOPY(t,s,n) g_free_and_dup(t->n, s->n)
#define SHALLOWCOPY(t,s,n) t->n = s->n

/** NOTE: copy_ldapserver ONLY copies the configuration
    information. It DOES NOT copy the operational stuff. Often you
    will want to call reset_ldapserver(target) alongside of
    copy_ldapserver.  */

void copy_ldapserver(GqServer *target,
		     const GqServer *source)
{
     /* configuration */

     DEEPCOPY   (P(target), P(source), name);
     DEEPCOPY   (P(target), P(source), host);
     SHALLOWCOPY(P(target), P(source), port);
     DEEPCOPY   (target, source, basedn);
     DEEPCOPY   (P(target), P(source), bind_dn);
     SHALLOWCOPY(P(target), P(source), bind_type);
     DEEPCOPY   (target, source, saslmechanism);
     DEEPCOPY   (P(target), P(source), search_attribute);
     SHALLOWCOPY(P(target), P(source), cache_connection);
     SHALLOWCOPY(P(target), P(source), use_tls);
     SHALLOWCOPY(target, source, local_cache_timeout);
     SHALLOWCOPY(P(target), P(source), ask_pw);
     SHALLOWCOPY(P(target), P(source), hide_internals);
     SHALLOWCOPY(target, source, show_ref);

     DEEPCOPY   (target, source, canon_name);
     SHALLOWCOPY(target, source, is_uri);
}

/* resets the operational stuff */
/** NOTE: reset_ldapserver sets the target refcount to 0 */
void
reset_ldapserver(GqServer *target)
{
     if (P(target)->connection) {
	  close_connection(target, TRUE);
     }
     target->incarnation = 0;
     target->missing_closes = 0;
     target->ss = NULL;
     target->flags = 0;
     target->version = LDAP_VERSION2;
     target->server_down = 0;
}

void canonicalize_ldapserver(GqServer *server)
{
     /* FIXME: should use better URI check */
     server->is_uri =  g_utf8_strchr(P(server)->host, -1, ':') != NULL;
     
     if (server->is_uri) {
	  g_free_and_dup(server->canon_name, P(server)->host);
     } else {
	  /* construct an LDAP URI */
	  GString *str = g_string_sized_new(100);
	  g_string_sprintf(str, "ldap://%s:%d/", 
			   P(server)->host, P(server)->port);
	  g_free(server->canon_name);
	  server->canon_name = str->str;
	  g_string_free(str, FALSE);
     }
}

/* GType */
G_DEFINE_TYPE(GqServer, gq_server, G_TYPE_OBJECT);

enum {
	PROP_0,
	PROP_ASK_PW,
	PROP_BIND_DN,
	PROP_BIND_TYPE,
	PROP_CACHE_CONNECTION,
	PROP_CONNECTED,
	PROP_HIDE_INTERNALS,
	PROP_HOST,
	PROP_NAME,
	PROP_PORT,
	PROP_SEARCH_ATTRIBUTE,
	PROP_USE_TLS
};

static void
gq_server_init(GqServer* self)
{
	/* well - not beautiful, but works */
	self->basedn = g_strdup("");
	self->saslmechanism = g_strdup("");
	gq_server_set_search_attribute(self, "cn");
	P(self)->cache_connection = DEFAULT_CACHECONN;
	P(self)->use_tls = DEFAULT_ENABLETLS;
	self->local_cache_timeout = DEFAULT_LOCAL_CACHE_TIMEOUT;
	P(self)->ask_pw = DEFAULT_ASK_PW;
	P(self)->hide_internals = DEFAULT_HIDE_INTERNAL;
	self->show_ref = DEFAULT_SHOW_REF;

	/* dynamic information */
	self->canon_name = g_strdup("");
	self->is_uri = 0;

	/* operational */
	self->incarnation = 0;
	self->missing_closes = 0;
	self->ss = NULL;
	self->flags = 0;
	self->version = LDAP_VERSION2;
	self->server_down = 0;
}

static void
server_finalize(GObject* object)
{
	GqServer* self = GQ_SERVER(object);

	if(P(self)->connection) {
		close_connection(self, 1);
	}

	if(P(self)->name) {
		g_free(P(self)->name);
		P(self)->name = NULL;
	}

	if(P(self)->host) {
		g_free(P(self)->host);
		P(self)->host = NULL;
	}

	g_free(self->basedn);

	g_free(P(self)->bind_dn);
	P(self)->bind_dn = NULL;

	g_free(self->saslmechanism);

	g_free(P(self)->search_attribute);
	P(self)->search_attribute = NULL;

	g_free(self->canon_name);

	G_OBJECT_CLASS(gq_server_parent_class)->finalize(object);
}

static void
server_get_property(GObject   * object,
		    guint       prop_id,
		    GValue    * value,
		    GParamSpec* pspec)
{
	GqServer* self = GQ_SERVER(object);

	switch(prop_id) {
	case PROP_ASK_PW:
		g_value_set_boolean(value, gq_server_get_ask_pw(self));
		break;
	case PROP_BIND_DN:
		g_value_set_string(value, gq_server_get_bind_dn(self));
		break;
	case PROP_BIND_TYPE:
		g_value_set_enum(value, gq_server_get_bind_type(self));
		break;
	case PROP_CACHE_CONNECTION:
		g_value_set_boolean(value, gq_server_get_cache_connection(self));
		break;
	case PROP_CONNECTED:
		g_value_set_boolean(value, gq_server_is_connected(self));
		break;
	case PROP_HIDE_INTERNALS:
		g_value_set_boolean(value, gq_server_get_hide_internals(self));
		break;
	case PROP_HOST:
		g_value_set_string(value, gq_server_get_host(self));
		break;
	case PROP_NAME:
		g_value_set_string(value, gq_server_get_name(self));
		break;
	case PROP_PORT:
		g_value_set_int(value, gq_server_get_port(self));
		break;
	case PROP_SEARCH_ATTRIBUTE:
		g_value_set_string(value, gq_server_get_search_attribute(self));
		break;
	case PROP_USE_TLS:
		g_value_set_boolean(value, gq_server_get_use_tls(self));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
server_set_property(GObject     * object,
		    guint         prop_id,
		    GValue const* value,
		    GParamSpec  * pspec)
{
	GqServer* self = GQ_SERVER(object);

	switch(prop_id) {
	case PROP_ASK_PW:
		gq_server_set_ask_pw(self, g_value_get_boolean(value));
		break;
	case PROP_BIND_DN:
		gq_server_set_bind_dn(self, g_value_get_string(value));
		break;
	case PROP_BIND_TYPE:
		gq_server_set_bind_type(self, g_value_get_enum(value));
		break;
	case PROP_CACHE_CONNECTION:
		gq_server_set_cache_connection(self, g_value_get_boolean(value));
		break;
	case PROP_CONNECTED:
		gq_server_set_connected(self, g_value_get_boolean(value));
		break;
	case PROP_HIDE_INTERNALS:
		gq_server_set_hide_internals(self, g_value_get_boolean(value));
		break;
	case PROP_HOST:
		gq_server_set_host(self, g_value_get_string(value));
		break;
	case PROP_NAME:
		gq_server_set_name(self, g_value_get_string(value));
		break;
	case PROP_PORT:
		gq_server_set_port(self, g_value_get_int(value));
		break;
	case PROP_SEARCH_ATTRIBUTE:
		gq_server_set_search_attribute(self, g_value_get_string(value));
		break;
	case PROP_USE_TLS:
		gq_server_set_use_tls(self, g_value_get_boolean(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
		break;
	}
}

static void
gq_server_class_init(GqServerClass* self_class)
{
	GObjectClass* object_class = G_OBJECT_CLASS(self_class);

	object_class->finalize     = server_finalize;
	object_class->get_property = server_get_property;
	object_class->set_property = server_set_property;

	g_object_class_install_property(object_class,
					PROP_ASK_PW,
					g_param_spec_boolean("ask-pw",
							     _("Ask Passwort"),
							     _("Ask for the Password when connecting"),
							     TRUE,
							     G_PARAM_READWRITE));
	g_object_class_install_property(object_class,
					PROP_BIND_DN,
					g_param_spec_string("bind-dn",
							    _("Bind DN"),
							    _("The distinguished name (DN) to bind with"),
							    NULL,
							    G_PARAM_READWRITE));
	g_object_class_install_property(object_class,
					PROP_BIND_TYPE,
					g_param_spec_enum("bind-type",
							  _("Bind Type"),
							  _("The mechanism to use when binding to the server"),
							  GQ_TYPE_BIND_TYPE,
							  GQ_BIND_SIMPLE,
							  G_PARAM_READWRITE));
	g_object_class_install_property(object_class,
					PROP_CACHE_CONNECTION,
					g_param_spec_string("cache-connection",
							    _("Cache Connection"),
							    _("Cache Connection to increase the application's speed"),
							    FALSE,
							    G_PARAM_READWRITE));
	g_object_class_install_property(object_class,
					PROP_CONNECTED,
					g_param_spec_boolean("connected",
							     _("Connected"),
							     _("Is a connecttion to the represented server established"),
							     FALSE,
							     G_PARAM_READWRITE));
#warning "FIXME: make G_PARAM_READBLE for connected"
	g_object_class_install_property(object_class,
					PROP_HIDE_INTERNALS,
					g_param_spec_boolean("hide-internals",
							     _("Hide Internals"),
							     _("Hide the internal attributes"),
							     FALSE,
							     G_PARAM_READWRITE));
	g_object_class_install_property(object_class,
					PROP_HOST,
					g_param_spec_string("host",
							    _("LDAP Host"),
							    _("The hostname of the server"),
							    "localhost",
							    G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
	g_object_class_install_property(object_class,
					PROP_NAME,
					g_param_spec_string("name",
							    _("Server Name"),
							    _("The displayed name of the server"),
							    NULL,
							    G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
	g_object_class_install_property(object_class,
					PROP_PORT,
					g_param_spec_int("port",
							 _("Server Port"),
							 _("The port of the server"),
							 1, G_MAXINT, 389,
							 G_PARAM_READWRITE | G_PARAM_CONSTRUCT));
	g_object_class_install_property(object_class,
					PROP_SEARCH_ATTRIBUTE,
					g_param_spec_string("search-attribute",
							    _("Search Attribute"),
							    _("The attribute to be used for simple searches"),
							    "cn",
							    G_PARAM_READWRITE));
	g_object_class_install_property(object_class,
					PROP_USE_TLS,
					g_param_spec_boolean("use-tls",
							     _("Use TLS"),
							     _("Use TLS when connecting"),
							     FALSE,
							     G_PARAM_READWRITE));

	g_type_class_add_private(self_class, sizeof(struct GqServerPrivate));
}

