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

VERSIONSTRING = '1.2'

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

LDAP_KEY_MAP = {}
LDAP_KEY_MAP['sn'] = 'Surname'
LDAP_KEY_MAP['givenName'] = 'Given name'
LDAP_KEY_MAP['mail'] = 'Email'
LDAP_KEY_MAP['homePhone'] = 'Home phone number'
LDAP_KEY_MAP['mobile'] = 'Mobile phone number'
LDAP_KEY_MAP['homePostalAddress'] = 'Home address'
LDAP_KEY_MAP['telephoneNumber'] = 'Work phone number'
LDAP_KEY_MAP['facsimileTelephoneNumber'] = 'Fax number'
LDAP_ALL_EDITABLE_KEYS = ['sn', 'givenName', "mail", "homePhone", "mobile", "telephoneNumber", "homePostalAddress","facsimileTelephoneNumber"]
LDAP_ALL_KEYS = ['cn', 'sn', 'givenName', "mail", "homePhone", "mobile", "telephoneNumber", "homePostalAddress","facsimileTelephoneNumber"]

class LdapBackend:
	"This class will do all actual ldap communication"
	def connect(self,debug=0):
		self.ld = ldap.initialize(self.ldapurl,trace_level=debug)
		self.ld.set_option(ldap.OPT_PROTOCOL_VERSION,3)
		if (self.binddn != None and self.bindpw != None):
			self.ld.simple_bind_s(self.binddn, self.bindpw)
	
	def __init__(self,ldapurl,binddn,bindpw,basedn,debug=0):
		self.ldapurl = ldapurl
		self.binddn = binddn
		self.bindpw = bindpw
		self.baseDN = 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,LDAP_ALL_KEYS)[0][1]
		except ldap.SERVER_DOWN:
			self.connect(0)
			entry = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,filter,LDAP_ALL_KEYS)[0][1]
		return entry

	def get_dnlist(self,name):
		if (len(name)>0):
			pat = '(cn=*'+name+'*)'
		else:
			pat = '(cn=*)'
		try:
			res = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,pat,['cn'])
		except:
			self.connect(0)
			res = self.ld.search_s(self.baseDN,ldap.SCOPE_SUBTREE,pat,['cn'])
		results = {}
		i=len(res)-1
		while (i >= 0):
			dn = res[i][0]
			cn = res[i][1]['cn'][0]
			results[dn] = cn
			i -= 1
		return results
	
	def saveAddress(self,cn,newaddress):
		modlist = []
		for key in LDAP_ALL_EDITABLE_KEYS:
			if (len(newaddress[key]) > 0):
				modlist.append((key, newaddress[key]))
		modlist.append(('objectClass', 'inetOrgPerson'))
		modlist.append(('cn', cn))
#		modlist.append(('sn', cn))
		dn = 'cn='+cn+','+self.baseDN
		try:
			self.ld.add_s(dn, modlist)
			return 1
		except ldap.INSUFFICIENT_ACCESS:
			return 0
		
	def modifyAddress(self,dn,oldaddress,newaddress):
		modlist = []
		for key in LDAP_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]))
		try:
			self.ld.modify_s(dn, modlist)
			return 1
		except ldap.INSUFFICIENT_ACCESS:
			return 0

class EditGui:
	"This class is the gtk gui for the editor"
	def getlistforkey(self,key):
		j = len(self.entry[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 LDAP_ALL_EDITABLE_KEYS:
			newaddress[key] = self.getlistforkey(key)
		return newaddress

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

	def namefromkey(self, key):
		return LDAP_KEY_MAP[key]

	def plusclicked(self,widget,key):
		tmp = gtk.Entry()
		self.entry[key].append(tmp)
		self.vbox[key].pack_start(tmp)
		tmp.show()
	
	def cancelclicked(self,data):
		self.window.destroy()

	def addentry(self, address, key, startat, editable):
		self.table.attach(gtk.Label(self.namefromkey(key)), 0,1,startat, startat+1)
		if (editable):
			button = gtk.Button('+')
			button.connect('clicked', self.plusclicked, key)
			self.entry[key] = []
			self.vbox[key] = gtk.VBox(gtk.TRUE)
			self.table.attach(self.vbox[key], 1,2,startat, startat+1)
			i=0
			if (not address.has_key(key)):
				self.entry[key].append(gtk.Entry())
				self.vbox[key].pack_start(self.entry[key][i])
			else:
				j = len(address[key])
				for i in range(0,j):
					self.entry[key].append(gtk.Entry())
					self.entry[key][i].set_text(address[key][i])
					self.vbox[key].pack_start(self.entry[key][i])
			self.table.attach(button, 2,3,startat, startat+1)
		else:
			if (address.has_key(key)):
				self.table.attach(gtk.Label(address[key][0]),1,2,startat, startat+1)
	
	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)
		self.table = gtk.Table(10, 3, gtk.FALSE)
		self.table.set_row_spacings(10)
		self.table.set_col_spacings(10)
		self.window.add(self.table)
		self.entry = {}
		self.vbox = {}
#		if (len(dn)>0):
#			self.addentry(address, 'cn',1,0)
#		else:
#			self.addentry(address, 'cn',1,1)
		i = 1
		for key in LDAP_ALL_EDITABLE_KEYS:
			self.addentry(address, key,i,1)
			i += 1
		bbox = gtk.HButtonBox()
		bbox.set_layout(gtk.BUTTONBOX_END)
		bbox.set_spacing(10)
		self.table.attach(bbox,0,3,9,10)
		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
	
	def delete_event(self, widget, data):
		return gtk.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
			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 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):
		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')
		if (entry.has_key('homePhone')):
			str += self.get_all(entry['homePhone'],'',' (home)\n')
		if (entry.has_key('mobile')):
			str += self.get_all(entry['mobile'],'',' (mobile)\n')
		if (entry.has_key('homePostalAddress')):
			str += self.get_all(entry['homePostalAddress'],'','\n')
		if (entry.has_key('telephoneNumber')):
			str += self.get_all(entry['telephoneNumber'],'',' (work)\n')
		if (entry.has_key('facsimileTelephoneNumber')):
			str += self.get_all(entry['facsimileTelephoneNumber'],'',' (fax)\n')
		self.label.set_markup(str)
	
	def searchClicked(self, bla):
		self.listm.clear()
		str = self.entry.get_text()
		tmp = self.lb.get_dnlist(str)
		iter = None
		for k, v in tmp.items():
			iter = self.listm.prepend()
			self.listm.set(iter, 0, v, 1, k)
		if (iter != None):
			selection = self.treev.get_selection()
			selection.select_iter(iter)
			self.treev.grab_focus()
	
	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, lb, startupSearch):
		self.lb = lb
		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(gtk.FALSE,10)
		self.window.add(vbox)
		# the search toolbar
		hbox = gtk.HBox()
		vbox.pack_start(hbox, gtk.FALSE, gtk.TRUE)
		try:
			image = gtk.Image()
			image.set_from_file('/usr/local/share/directoryassistant/decoration.png')
			hbox.pack_start(image)
		except:
			pass
		label = gtk.Label("Search for")
		hbox.pack_start(label)
		self.entry = gtk.Entry()
		if (startupSearch != None):
			self.entry.set_text(startupSearch)
		self.entry.connect("activate", self.searchClicked)
		hbox.pack_start(self.entry)
		
		# the paned
		paned = gtk.HPaned()
		vbox.pack_start(paned, gtk.TRUE, gtk.TRUE)
		#the left pane
		scrolpane = gtk.ScrolledWindow()
		self.listm = gtk.ListStore(str,str)
		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(gtk.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-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,gtk.FALSE,gtk.TRUE)
		self.window.show_all()
		if (startupSearch != None):
			self.searchClicked(None)
		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, gtk.TRUE, gtk.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"
	def check(self):
		filename = os.getenv('HOME')+'/.directoryassistant'
		self.read((filename, '/etc/directoryassistant'))
		if self.has_section('main'):
			if self.has_option('main', 'ldapurl'):
				if self.has_option('main', 'base_dn'):
					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 fill the 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()):
		ldapurl = cfg.get('main', 'ldapurl')
		basedn = cfg.get('main', 'base_dn')
		try:
			binddn = cfg.get('main', 'bind_dn')
			bindpw = cfg.get('main', 'bind_password')
		except ConfigParser.NoOptionError:
			binddn = None
			bindpw = None
		try:
			startupSearch = cfg.get('main', 'startup_search')
		except ConfigParser.NoOptionError:
			startupSearch = None
		lb = LdapBackend(ldapurl,binddn,bindpw,basedn,0)
		ag = AddressGui(lb, startupSearch)
		ag.main()
	else:
		print 'YOUR CONFIG '+os.getenv('HOME')+'/.directoryassistant IS BROKEN'
