#!/usr/local/bin/python2.4
#
# Directory Assistant 2.0 - Copyright 2003,2004,2005 Olivier Sessink
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted as stated in the included COPYRIGHT file.
#

VERSIONSTRING = '2.0'

import gtk
import gobject
import ldap
import string
import ConfigParser
import os

class field:
	def __init__(self,name,title='',editable=0,multiline=0):
		self.name = name
		self.title = title
		self.editable = editable
		self.multiline = multiline

LDAP_KEY_MAP = []
LDAP_KEY_MAP.append(field('cn','Common name',0,0))
LDAP_KEY_MAP.append(field('sn','Surname',1,0))
LDAP_KEY_MAP.append(field('givenName','Given name',1,0))
LDAP_KEY_MAP.append(field('o','Organisation',1,0))
LDAP_KEY_MAP.append(field('mail','Email',1,0))
LDAP_KEY_MAP.append(field('telephoneNumber','Work phone number',1,0))
LDAP_KEY_MAP.append(field('facsimileTelephoneNumber','Fax number',1,0))
LDAP_KEY_MAP.append(field('mobile','Mobile phone number',1,0))
LDAP_KEY_MAP.append(field('street','Street',1,0))
LDAP_KEY_MAP.append(field('postalAddress','Postal address',1,1))
LDAP_KEY_MAP.append(field('postalCode','Postal code',1,0))
LDAP_KEY_MAP.append(field('l','Locality',1,0))
LDAP_KEY_MAP.append(field('homePhone','Home phone number',1,0))
LDAP_KEY_MAP.append(field('homePostalAddress','Home address',1,1))
LDAP_KEY_MAP.append(field('description','Description',1,1))

def all_keys():
	ret = []
	for field in LDAP_KEY_MAP:
		ret = ret + [field.name]
	return ret

def all_editable_keys():
	ret = []
	for field in LDAP_KEY_MAP:
		if (field.editable):
			ret = ret + [field.name]
	return ret

def field_for_key(key):
	for field in LDAP_KEY_MAP:
		if (field.name == key):
			return field
	return None

class MyEntry:
	widget = None
	multiline = 0
	
	def __init__(self,multiline=0):
		self.multiline = multiline
		if (self.multiline):
			self.buffer = gtk.TextBuffer()
			self.view = gtk.TextView(self.buffer)
			self.widget = gtk.ScrolledWindow()
			self.widget.add(self.view)
			self.widget.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC)
			self.widget.set_shadow_type(gtk.SHADOW_ETCHED_IN)
		else:
			self.widget = gtk.Entry()

	def get_text(self):
		if (self.multiline):
			iters = self.buffer.get_bounds()
			return self.buffer.get_text(iters[0],iters[1])
		else:
			return self.widget.get_text()
	
	def set_text(self,text):
		if (self.multiline):
			self.buffer.set_text(text)
		else:
			self.widget.set_text(text)

class LdapBackend:
	"This class will do all actual ldap communication"
	def connect(self,debug=0):
		print 'connect to ',self.ldapurl,'with version',self.ldapversion
		self.ld = ldap.initialize(self.ldapurl,trace_level=debug)
		if (self.ldapversion != None):
			self.ld.set_option(ldap.OPT_PROTOCOL_VERSION,self.ldapversion)
		if (self.binddn != None and self.bindpw != None):
			print 'try user ',self.binddn
			try:
				self.ld.simple_bind_s(self.binddn, self.bindpw)
			except ldap.INVALID_CREDENTIALS:
				print 'Invalid ldap user or password'
	
	def __init__(self,cfg,section,debug=0):
		self.name = section
		self.ldapurl = cfg.get(section, 'ldapurl')
		self.baseDN = cfg.get(section, 'base_dn')
		try:
			self.binddn = cfg.get(section, 'bind_dn')
			self.bindpw = cfg.get(section, 'bind_password')
		except ConfigParser.NoOptionError:
			self.binddn = None
			self.bindpw = None
		try:
			self.ldapversion = int(cfg.get(section, 'ldapversion'))
		except (ConfigParser.NoOptionError,ValueError):
			self.ldapversion = None
		try:
			self.addDN = cfg.get(section,'add_dn')
		except (ConfigParser.NoOptionError):
			self.addDN = self.baseDN
		self.connect(debug)
	
	def get_address(self,dn):
		filter = dn[:string.find(dn,',')]
		try:
			entry = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,filter,all_keys())[0][1]
		except ldap.SERVER_DOWN:
			self.connect(0)
			entry = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,filter,all_keys())[0][1]
		return entry

	def get_dnlist_name(self,name):
		if (len(name)>0):
			pat = '(|(cn=*'+name+'*)(sn=*'+name+'*)(givenName=*'+name+'*))'
		else:
			pat = '(cn=*)'
		print 'searching for '+pat+' in '+self.baseDN
		try:
			res = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,pat,['cn'])
		except ldap.SERVER_DOWN:
			self.connect(0)
			res = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,pat,['cn'])
		results = {}
		i=len(res)-1
		print 'found ',len(res),'results'
		while (i >= 0):
			dn = res[i][0]
			cn = res[i][1]['cn'][0]
			results[dn] = cn
			i -= 1
		return results
	
	def get_dnlist_organisation(self,name):
		if (len(name)>0):
			pat = '(o=*'+name+'*)'
		else:
#			pat = '(&(cn=*)(o=*))'
			pat = '(o=*)'
		print 'searching for '+pat+' in '+self.baseDN
		try:
			res = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,pat,['o'])
		except ldap.SERVER_DOWN:
			self.connect(0)
			res = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,pat,['o'])
		results = {}
		i=len(res)-1
		while (i >= 0):
			dn = res[i][0]
			cn = res[i][1]['o'][0]
			results[dn] = cn
			i -= 1
		return results
	
	def saveAddress(self,cn,newaddress):
		modlist = []
		for key in all_editable_keys():
			if (len(newaddress[key]) > 0):
				modlist.append((key, newaddress[key]))
		modlist.append(('objectClass', 'inetOrgPerson'))
		modlist.append(('cn', cn))
		dn = 'cn='+cn+','+self.addDN
		try:
			self.ld.add_s(dn, modlist)
			return 1
		except ldap.SERVER_DOWN:
			self.connect(0)
			self.ld.add_s(dn, modlist)
			return 1
		except ldap.INSUFFICIENT_ACCESS:
			return 0
		
	def modifyAddress(self,dn,cn,oldaddress,newaddress):
		modlist = []
		for key in all_editable_keys():
			if (len(newaddress[key]) == 0 and oldaddress.has_key(key) and len(oldaddress[key]) > 0):
				modlist.append((ldap.MOD_DELETE,key,()))
			else:
				modlist.append((ldap.MOD_REPLACE, key, newaddress[key]))
# now check if the dn should be updated
		pos = string.find(dn, ',')
		if (pos > 0):
			new_dn = 'cn='+cn+','+dn[pos+1:]
		else:
			new_dn = 'cn='+cn+','+self.addDN
		try:
			self.ld.modify_s(dn, modlist)
			if (new_dn != dn):
				new_rdn = ldap.explode_dn(new_dn)[0]
				self.ld.modrdn_s(dn, new_rdn)
			return 1
		except ldap.INSUFFICIENT_ACCESS:
			return 0

	def deleteAddress(self,dn):
		try:
			self.ld.delete_s(dn)
		except ldap.LDAPError, e:
			print e

class EditGui:
	"This class is the gtk gui for the editor"
	
	def getlistforkey(self,key):
		j = len(self.entry[key])
		field = field_for_key(key)
		lst = []
		for i in range(0,j):
			tmp = self.entry[key][i].get_text()
			if (len(tmp)>0):
				lst.append(tmp)
		return lst
	
	def getnewaddress(self):
		newaddress = {}
		for key in all_editable_keys():
			newaddress[key] = self.getlistforkey(key)
		return newaddress

	def saveclicked(self,data):
		newaddress = self.getnewaddress()
		ret = 0
		cn = self.entry['sn'][0].get_text()+ ' '+self.entry['givenName'][0].get_text()
		try:
			if (self.dn):
				self.lb.modifyAddress(self.dn,cn,self.address,newaddress)
			else:
				self.lb.saveAddress(cn, newaddress)
			self.window.destroy()
			return
		except ldap.SERVER_DOWN:
			message = 'address server down'
		except ldap.INSUFFICIENT_ACCESS:
			message = 'Permission was denied while trying to save the address'
		dialog = gtk.MessageDialog(parent=self.window, flags=gtk.DIALOG_DESTROY_WITH_PARENT, 
					type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK,message_format = message)
		dialog.set_title('Error')
		dialog.connect('response', lambda dialog, response: dialog.destroy())
		dialog.show_all()

	def plusclicked(self,widget,key):
		field = field_for_key(key)
		tmp = MyEntry(field.multiline)
		self.entry[key].append(tmp)
		self.vbox[key].pack_start(tmp.widget)
		tmp.widget.show()
	
	def cancelclicked(self,data):
		self.window.destroy()

	def addfieldentry(self,address,field,table,startat):
#		print 'adding ',field.title,field.name,'at startat=',startat
		table.attach(gtk.Label(field.title), 0,1,startat, startat+1,xoptions=gtk.FILL)
		if (field.editable):
			button = gtk.Button('+')
			button.connect('clicked', self.plusclicked, field.name)
			self.entry[field.name] = []
			self.vbox[field.name] = gtk.VBox(True)
			table.attach(self.vbox[field.name], 1,2,startat, startat+1,xoptions=gtk.EXPAND|gtk.FILL)
			i=0
			if (not address.has_key(field.name)):
				self.entry[field.name].append(MyEntry(field.multiline))
				self.vbox[field.name].pack_start(self.entry[field.name][i].widget)
			else:
				j = len(address[field.name])
				for i in range(0,j):
					self.entry[field.name].append(MyEntry(field.multiline))
					self.entry[field.name][i].set_text(address[field.name][i])
					self.vbox[field.name].pack_start(self.entry[field.name][i].widget)
			table.attach(button, 2,3,startat, startat+1,xoptions=gtk.FILL,yoptions=0)
		else:
			if (address.has_key(field.name)):
				table.attach(gtk.Label(address[field.name][0]),1,2,startat, startat+1,xoptions=gtk.FILL)
	
	def __init__(self, lb, dn, address):
		self.address = address
		self.dn = dn
		self.lb = lb
		self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
		self.window.set_title("Edit Address")
		self.window.set_border_width(10)
		vbox1 = gtk.VBox(False, True)
		self.window.add(vbox1)
		hbox1 = gtk.HBox(True, True)
		vbox1.pack_start(hbox1)
		numkeys = len(LDAP_KEY_MAP)
		self.ltable = gtk.Table((numkeys/2)+1, 3, False)
		self.ltable.set_row_spacings(6)
		self.ltable.set_col_spacings(6)
		hbox1.pack_start(self.ltable)
		self.rtable = gtk.Table((numkeys/2)+1, 3, False)
		self.rtable.set_row_spacings(6)
		self.rtable.set_col_spacings(6)
		hbox1.pack_start(self.rtable)
		self.entry = {}
		self.vbox = {}
		i = 0
		for field in LDAP_KEY_MAP:
			print field.name
			if (i > numkeys/2):
				self.addfieldentry(address,field,self.rtable,i-numkeys/2)
			else:
				self.addfieldentry(address,field,self.ltable,i)
			i = i + 1
		bbox = gtk.HButtonBox()
		bbox.set_layout(gtk.BUTTONBOX_END)
		bbox.set_spacing(10)
		vbox1.pack_start(bbox)
		button = gtk.Button('gtk-cancel')
		button.set_use_stock(1)
		button.connect('clicked',self.cancelclicked)
		bbox.add(button)
		button = gtk.Button('gtk-save')
		button.set_use_stock(1)
		button.connect('clicked',self.saveclicked)
		bbox.add(button)
		self.window.show_all()

class AddressGui:
	"This class is the GTK GUI for the main window"
	dn = None
	address = None
	lb = None
	cfg = None
	
	def delete_event(self, widget, data):
		return False
		
	def destroy(self, widget, data=None):
		gtk.main_quit()
	
	def treevClicked(self,treev,event):
		if (event.type == 4):
			# 4 seems to be single-click, so we refresh
			if (self.dn != None):
				self.address = self.lb.get_address(self.dn)
				self.set_address_label(self.address)
		# 5 seems to be doubleclick, and 6 tripleclick
		if (event.type >= 5):
			if (self.dn):
				ea = EditGui(self.lb,self.dn,self.address)
	
	def newClicked(self,bla):
		ea = EditGui(self.lb,'',{})

	def deleteClicked(self,bla):
		if (self.dn != None):
			self.lb.deleteAddress(self.dn)
			self.dn = None
			self.address = None
			self.searchClicked(None)	

	def get_all(self,entry,prefix,suffix):
		str = ''
		i=len(entry)-1
		if (i >= 0):
			while (i >= 0):
				str += prefix+self.prepare(entry[i])+suffix
				i -= 1
		return str
	
	def prepare(self,str):
		str = string.replace(str,'&', '&amp;')
		str = string.replace(str,'<', '&lt;')
		str = string.replace(str,'>', '&gt;')
		return str
	
	def set_address_label(self,entry):
		print 'set_address_label to ',entry
		if (entry.has_key('cn')):
			str = '<b>'+self.prepare(entry['cn'][0])+'</b>\n'
			if (entry.has_key('mail')):
				str += self.get_all(entry['mail'],'<span foreground="blue">','</span>\n')
			for key in all_keys():
				if (not key in ['cn', 'sn', 'givenName', 'mail','o']):
					if (entry.has_key(key)):
						str += self.get_all(entry[key],'',' \n')
		elif (entry.has_key('o')):
			str = '<b>'+self.prepare(entry['o'][0])+'</b>\n'
			for key in all_keys():
				if (not key in ['o']):
					if (entry.has_key(key)):
						str += self.get_all(entry[key],'',' \n')
		print 'set str to',str
		self.label.set_markup(str)
	
	def searchClicked(self, bla):
		if (self.ldservers != None):
			server = self.ldservers.get_active_text()
			if (self.lb.name != server):
				print 'switch to new server '+server
				self.lb = LdapBackend(self.cfg,server,0)
		self.listm.clear()
		str = self.entry.get_text()
		try:
			tmp1 = self.lb.get_dnlist_name(str)
			iter = None
			for k, v in tmp1.iteritems():
				iter = self.listm.prepend()
				self.listm.set(iter, 0, v, 1, k)
	#			print '(cn) adding '+k+' with v='+v
			tmp2 = self.lb.get_dnlist_organisation(str)
			for k, v in tmp2.iteritems():
				if (k not in tmp1):
					iter = self.listm.prepend()
					self.listm.set(iter, 0, v, 1, k)
					print '(o) adding '+k+' with v='+v
				else:
					print '(o) NOT adding '+k+' with v='+v
			if (iter != None):
				selection = self.treev.get_selection()
				selection.select_iter(iter)
				self.treev.grab_focus()
		except ldap.SERVER_DOWN:
			self.label.set_markup('<span foreground="red"><b>address server not available</b></span>')
	
	def closeClicked(self,bla):
		self.window.destroy()

	def selectionChanged(self, data):
		store = data.get_selected()[0]
		iter = data.get_selected()[1]
		try:
			self.dn = store.get_value(iter,1)
			self.address = self.lb.get_address(self.dn)
			self.set_address_label(self.address)
		except:
			self.dn = None
			self.address = None
			self.label.set_markup("no result")
		
	def __init__(self, cfg):
		self.cfg = cfg
		self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
		self.window.set_title("Directory Assistant "+VERSIONSTRING)
		self.window.set_border_width(10)
		try:
			pixbuf = gtk.gdk.pixbuf_new_from_file('/usr/local/share/directoryassistant/directoryassistant.png')
			self.window.set_icon(pixbuf)
		except:
			pass
		self.window.connect('delete_event', self.delete_event)
		self.window.connect('destroy', self.destroy)
		self.window.set_border_width(10)
		vbox = gtk.VBox(False,10)
		self.window.add(vbox)
		# the search toolbar
		hbox = gtk.HBox()
		vbox.pack_start(hbox, False, True)
		try:
			image = gtk.Image()
			image.set_from_file('/usr/local/share/directoryassistant/decoration.png')
			hbox.pack_start(image)
		except:
			pass
		self.table = gtk.Table(2, 2, False)
		self.table.set_row_spacings(10)
		self.table.set_col_spacings(10)
		hbox.pack_start(self.table)

		label = gtk.Label("Search for")
		self.table.attach(label,0,1,0,1)
		self.entry = gtk.Entry()
		self.entry.connect("activate", self.searchClicked)
		self.table.attach(self.entry,1,2,0,1)
		if (cfg.numvalidsections >1):
			label = gtk.Label("in address book")
			self.table.attach(label,0,1,1,2)
			self.ldservers = gtk.combo_box_new_text()
			for sect in cfg.sections():
				if (self.lb == None):
					self.lb = LdapBackend(self.cfg,sect,0)
				self.ldservers.prepend_text(sect)
			self.ldservers.set_active(0)
			self.table.attach(self.ldservers,1,2,1,2)
		else:
			self.ldservers = None
			for sect in cfg.sections():
				self.lb = LdapBackend(self.cfg,sect,0)
				break
		# the paned
		paned = gtk.HPaned()
		vbox.pack_start(paned, True, True)
		#the left pane
		scrolpane = gtk.ScrolledWindow()
		self.listm = gtk.ListStore(str,str)
		self.listm.set_sort_column_id(1, gtk.SORT_ASCENDING)
		self.treev = gtk.TreeView(self.listm)
		scrolpane.set_size_request(150,200)
		rend = gtk.CellRendererText()
		column = gtk.TreeViewColumn('Results', rend, text=0)
		self.treev.append_column(column)
		selection = self.treev.get_selection()
		selection.set_mode(gtk.SELECTION_SINGLE)
		selection.connect('changed', self.selectionChanged)
		self.treev.connect('button_press_event',self.treevClicked)
		scrolpane.add_with_viewport(self.treev)
		paned.add1(scrolpane)
		#the right pane
		frame = gtk.Frame()
		frame.set_shadow_type(gtk.SHADOW_IN)
		self.label = gtk.Label()
		self.label.set_size_request(250,200)
#		self.label.set_justify(gtk.JUSTIFY_LEFT)
#		self.label.set_alignment(xalign=0.5, yalign=0.5) 
		self.label.set_selectable(True)
		self.label.set_markup("no results yet");
		frame.add(self.label)
		paned.add2(frame)
		# buttonbox
		bbox = gtk.HButtonBox()
		bbox.set_layout(gtk.BUTTONBOX_END)
		bbox.set_spacing(10)
		button = gtk.Button('gtk-delete')
		button.set_use_stock(1)
		bbox.add(button)
		button.connect('clicked', self.deleteClicked)
		button = gtk.Button('gtk-new')
		button.set_use_stock(1)
		bbox.add(button)
		button.connect('clicked', self.newClicked)
		button = gtk.Button('gtk-close')
		button.connect('clicked', self.closeClicked)
		button.set_use_stock(1)
		bbox.add(button)
		button = gtk.Button('gtk-find')
		button.set_use_stock(1)
		button.connect('clicked',self.searchClicked)
		button.set_flags(gtk.CAN_DEFAULT)
		self.window.set_default(button)
		bbox.add(button)
		vbox.pack_start(bbox,False,True)
		self.window.show_all()
		try:
			self.entry.set_text(cfg.get('main', 'startup_search'))
			self.searchClicked(None)
		except:
			pass
		self.entry.grab_focus()

	def main(self):
		gtk.main()

class ErrorMessage:
	def quit(self, wid, data):
		gtk.main_quit()
	def __init__(self, message):
		dialog = gtk.Dialog(title="ERROR", flags=gtk.DIALOG_MODAL, buttons=(gtk.STOCK_OK, 1))
		label = gtk.Label()
		label.set_markup('<b>'+message+'</b>')
		dialog.vbox.pack_start(label, True, True, 0)
		dialog.connect('close',self.quit)
		dialog.connect('response',self.quit)
		dialog.show_all()
		gtk.main()


class MyConfigParser(ConfigParser.ConfigParser):
	"This class will make sure the configfile is correct"
	numvalidsections = 0
	
	def section_is_correct(self,section):
		if (self.has_option(section,'ldapurl') and self.has_option(section,'base_dn')):
			return 1
		return 0
	
	def check(self):
		filename = os.getenv('HOME')+'/.directoryassistant'
		self.read((filename, '/etc/directoryassistant'))
		for sect in self.sections():
			if (self.section_is_correct(sect)):
				self.numvalidsections+=1
			else:
				self.remove_section(sect)
		if (self.numvalidsections >0):
			return 1
		if (os.path.exists(filename)):
			message = 'Your configfile '+filename+' does not contain\nboth the required fields ldapurl and basedn.'
		else:
			message = 'Your configfile '+filename+' did not yet exist, an empty config file is created, but you have to set the correct values.'
			fd = open(filename, 'w')
			fd.write("[main]\n#ldapurl = ldap://myserver/\n#bind_dn = cn=myaccount,o=myorg\n#bind_password = mysecret\n#base_dn = ou=Mydepartment,o=myorg\n#startup_search=myname")
			fd.close()
		em = ErrorMessage(message)
		return 0

if __name__ == "__main__":
	cfg = MyConfigParser()
	if (cfg.check()):
		ag = AddressGui(cfg)
		ag.main()
	else:
		print 'Please create '+os.getenv('HOME')+'/.directoryassistant file,\nfollowing the format below:'
		print """
[My Address Book]
ldapurl = ldap://your.ldap.server.com/
bind_dn = cn=someaccount,o=someorg
bind_password = yourpassword'
base_dn = ou=department,ou=People,o=someorg"""
