#!/usr/bin/env python # # Revelation - a password manager for GNOME 2 # http://oss.codepoet.no/revelation/ # $Id$ # # Applet for account lookup # # # Copyright (c) 2003-2006 Erik Grinaker # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # 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 General Public License for more details. # # You should have received a copy of the GNU 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. # import gettext, gnome, gnomeapplet, gobject, gtk, os, sys if "@pyexecdir@" not in sys.path: sys.path.insert(0, "@pyexecdir@") from revelation import config, data, datahandler, dialog, entry, io, ui, util _ = gettext.gettext class RevelationApplet(object): "Revelation applet" def __init__(self, applet, iid): self.applet = applet self.iid = iid sys.excepthook = self.__cb_exception gettext.bindtextdomain(config.PACKAGE, config.DIR_LOCALE) gettext.bind_textdomain_codeset(config.PACKAGE, "UTF-8") gettext.textdomain(config.PACKAGE) try: self.__init_config() self.__init_facilities() self.__init_ui() self.__init_states() except config.ConfigError: dialog.Error(None, _('Missing configuration data'), _('The applet could not find its configuration data, please reinstall Revelation.')).run() sys.exit(1) def __init_config(self): "Sets up configuration" self.applet.add_preferences("/schemas/apps/revelation-applet/prefs") self.config = config.Config(self.applet.get_preferences_key()) # set up defaults # TODO this shouldn't really be necessary, the schema should # be used for defaults - is this even possible with the current # applet api? defaults = { "autolock" : True, "autolock_timeout" : 10, "chain_username" : False, "file" : "", "menuaction" : "show", "show_passwords" : True, "show_searchentry" : True } for key, value in defaults.items(): try: self.config.get(key) except config.ConfigError: self.config.set_force(key, value) # make sure the launchers have been set up, otherwise # install the Revelation schema def check_launchers(): try: for entrytype in [ et for et in entry.ENTRYLIST if et != entry.FolderEntry ]: self.config.get("/apps/revelation/launcher/%s" % entrytype.id) except config.ConfigError: return False else: return True if check_launchers() == False: if config.install_schema("%s/revelation.schemas" % config.DIR_GCONFSCHEMAS) == False: raise config.ConfigError if check_launchers() == False: raise config.ConfigError def __init_facilities(self): "Sets up facilities" self.clipboard = data.Clipboard() self.datafile = io.DataFile(datahandler.Revelation) self.entrystore = data.EntryStore() self.entrysearch = data.EntrySearch(self.entrystore) self.items = ui.ItemFactory(self.applet) self.locktimer = data.Timer() self.config.monitor("autolock_timeout", lambda k,v,d: self.locktimer.start(v * 60)) self.config.monitor("file", self.__cb_config_file) self.datafile.connect("changed", self.__cb_file_changed) self.datafile.connect("content-changed", self.__cb_file_content_changed) self.locktimer.connect("ring", self.__cb_file_autolock) self.entrysearch.folders = False def __init_states(self): "Sets up the initial states" self.datafile.emit("changed", self.datafile.get_file()) os.chdir(os.path.expanduser("~/")) self.config.monitor("show_searchentry", self.__cb_config_show_searchentry) def __init_ui(self): "Sets up the main ui" gtk.about_dialog_set_url_hook(lambda d,l: gnome.url_show(l)) gtk.about_dialog_set_email_hook(lambda d,l: gnome.url_show("mailto:" + l)) # set up applet self.applet.set_flags(gnomeapplet.EXPAND_MINOR) # set up window icons pixbufs = [ self.items.get_pixbuf("revelation", size) for size in ( 48, 32, 24, 16) ] pixbufs = [ pixbuf for pixbuf in pixbufs if pixbuf != None ] if len(pixbufs) > 0: gtk.window_set_default_icon_list(*pixbufs) # set up popup menu self.applet.setup_menu(""" """, ( ( "about", lambda w,d=None: self.about() ), ( "file-lock", lambda w,d=None: self.file_close() ), ( "file-reload", lambda w,d=None: self.file_reload() ), ( "file-unlock", lambda w,d=None: self.file_open(self.config.get("file")) ), ( "prefs", lambda w,d=None: self.prefs() ), ( "revelation", lambda w,d=None: util.execute_child("@bindir@/revelation") ), ), None) # set up ui items self.entry = ui.Entry() self.entry.set_width_chars(14) self.entry.connect("activate", self.__cb_entry_activate) self.entry.connect("button_press_event", self.__cb_entry_buttonpress) self.entry.connect("key_press_event", lambda w,d=None: self.locktimer.reset()) self.icon = ui.Image() self.eventbox = ui.EventBox(self.icon) self.eventbox.connect("button_press_event", self.__cb_icon_buttonpress) self.hbox = ui.HBox(self.eventbox, self.entry) self.applet.add(self.hbox) self.applet.show_all() # set up various ui element holders self.popup_entryview = None self.popup_entrylist = None self.entrymenu = None ##### CALLBACKS ##### def __cb_config_file(self, key, value, data): "Config callback for file key changes" self.file_close() self.applet.get_popup_component().set_prop("/commands/file-unlock", "sensitive", self.config.get("file") != "" and "1" or "0") def __cb_config_show_searchentry(self, key, value, data): "Config callback for show searchentry setting" if value == True: self.entry.show() else: self.entry.hide() def __cb_exception(self, type, value, trace): "Callback for unhandled exceptions" if type == KeyboardInterrupt: sys.exit(1) traceback = util.trace_exception(type, value, trace) sys.stderr.write(traceback) if dialog.Exception(None, traceback).run() == True: gtk.main() else: sys.exit(1) def __cb_entry_activate(self, widget, data = None): "Callback for entry activation (pressing enter etc)" self.entry_search(self.entry.get_text(), True) def __cb_entry_buttonpress(self, widget, data = None): "Callback for entry button presses" self.locktimer.reset() if data.button == 1: self.applet.request_focus(data.time) def __cb_file_autolock(self, widget, data = None): "Callback for autolocking the file" if self.config.get("autolock") == True: self.file_close() def __cb_file_content_changed(self, widget, data = None): "Callback for changed file content" try: self.__file_load(self.datafile.get_file(), self.datafile.get_password()) except dialog.CancelError: pass except datahandler.PasswordError: self.file_close() except datahandler.Error: pass def __cb_file_changed(self, widget, data = None): "Callback for changed data file" popup = self.applet.get_popup_component() if self.datafile.get_file() == None: self.entry.set_text("") popup.set_prop("/commands/file-unlock", "sensitive", self.config.get("file") != "" and "1" or "0") popup.set_prop("/commands/file-lock", "sensitive", "0") popup.set_prop("/commands/file-reload", "sensitive", "0") self.icon.set_from_stock(ui.STOCK_REVELATION_LOCKED, ui.ICON_SIZE_APPLET) else: popup.set_prop("/commands/file-unlock", "sensitive", "0") popup.set_prop("/commands/file-lock", "sensitive", "1") popup.set_prop("/commands/file-reload", "sensitive", "1") self.icon.set_from_stock(ui.STOCK_REVELATION, ui.ICON_SIZE_APPLET) def __cb_icon_buttonpress(self, widget, data = None): "Callback for buttonpress on button" if data.button != 1: return False self.entry_menu(data.time) return True def __cb_popup_activate(self, widget, data = None): "Takes appropriate action when a menu item is activated" self.locktimer.reset() action = self.config.get("menuaction") if action == "show": self.entry_show(data) elif action == "copy": self.entry_copychain(data) elif self.__launcher_valid(data): self.entry_goto(data) else: self.entry_show(data) ##### PRIVATE METHODS ##### def __close_popups(self): "Closes any open popups" self.locktimer.reset() if hasattr(self, "popup_entryview") == True and self.popup_entryview != None: self.popup_entryview.destroy() if hasattr(self, "popup_entrylist") == True and self.popup_entrylist != None: self.popup_entrylist.destroy() if hasattr(self, "entrymenu") == True and self.entrymenu != None: self.entrymenu.hide() def __file_load(self, file, password = None): "Loads a data file" if file in ( "", None ): return False if dialog.present_unique(dialog.PasswordOpen) == True: return False entrystore = self.datafile.load(file, password, lambda: dialog.run_unique(dialog.PasswordOpen, None, os.path.basename(file))) self.entrystore.clear() self.entrystore.import_entry(entrystore, None) self.entrymenu = self.__generate_entrymenu(self.entrystore) self.locktimer.start(self.config.get("autolock_timeout") * 60) self.__close_popups() return True def __flash_entry(self, color = "#ffbaba", duration = 500): "Flashes the entry with a color" color_normal = ui.Entry().rc_get_style().base[gtk.STATE_NORMAL] color_new = gtk.gdk.color_parse(color) self.entry.modify_base(gtk.STATE_NORMAL, color_new) gobject.timeout_add(duration, lambda: self.entry.modify_base(gtk.STATE_NORMAL, color_normal)) def __focus_entry(self): "Gives focus to the entry" self.applet.request_focus(long(0)) def __generate_entrymenu(self, entrystore, parent = None): "Generates an entry menu tree" menu = gtk.Menu() for i in range(entrystore.iter_n_children(parent)): iter = entrystore.iter_nth_child(parent, i) e = entrystore.get_entry(iter) item = ui.ImageMenuItem(type(e) == entry.FolderEntry and ui.STOCK_FOLDER or e.icon, e.name) item.connect("select", lambda w,d=None: self.locktimer.reset()) if type(e) == entry.FolderEntry: item.set_submenu(self.__generate_entrymenu(entrystore, iter)) else: item.connect("activate", self.__cb_popup_activate, e) menu.append(item) return menu def __get_launcher(self, e): "Returns a launcher command for an entry, if possible" command = self.config.get("/apps/revelation/launcher/%s" % e.id) if command in ( "", None ): return None subst = {} for field in e.fields: subst[field.symbol] = field.value command = util.parse_subst(command, subst) return command def __get_popup_offset(self, popup): "Returns a tuple of x and y offset coords for popups" screen = self.applet.get_screen() a = self.applet.get_allocation() rw, rh = popup.size_request() x, y = self.applet.window.get_origin() x += a.x y += a.y # TODO use constants ORIENT_UP etc here, if available if self.applet.get_orient() in ( 0, 1 ): x = min(x, screen.get_width() - rw) if (y > screen.get_height() / 2): y -= rh else: y += a.height else: y = min(y, screen.get_height() - rh) if (x > screen.get_width() / 2): x -= rw else: x += a.width return x, y def __launcher_valid(self, e): "Checks if a launcher is valid" try: command = self.__get_launcher(e) return command != None except ( util.SubstFormatError ): return True except ( util.SubstValueError, config.ConfigError ): return False def __require_file(self): "Checks if a datafile is loaded, or alerts the user" if self.datafile.get_file() != None: return True if self.config.get("file") != "": return self.file_open(self.config.get("file")) d = dialog.Info( None, _('File not selected'), _('You must select a Revelation data file to use - this can be done in the applet preferences.'), ( ( gtk.STOCK_PREFERENCES, gtk.RESPONSE_ACCEPT ), ( gtk.STOCK_OK, gtk.RESPONSE_OK ) ) ) if d.run() == gtk.RESPONSE_ACCEPT: self.prefs() return False ##### PUBLIC METHODS ##### def about(self): "Displays an about dialog" dialog.run_unique(About, self.applet) def entry_copychain(self, e, launcher = ""): "Copies all passwords from an entry as a chain" if e == None: return secrets = [ field.value for field in e.fields if field.datatype == entry.DATATYPE_PASSWORD and field.value != "" ] if self.config.get("chain_username") == True and len(secrets) > 0: if e.has_field(entry.UsernameField) and e[entry.UsernameField] != "": if "%" + entry.UsernameField.symbol not in launcher: secrets.insert(0, e[entry.UsernameField]) self.clipboard.set(secrets) def entry_goto(self, e): "Goes to an entry" try: command = self.__get_launcher(e) if command == None: return self.entry_copychain(e) util.execute_child(command) except ( util.SubstFormatError, config.ConfigError ): dialog.Error(None, _('Invalid goto command format'), _('The goto command for '" + e.typename + "' entries is invalid, please correct this in the preferences.')).run() except util.SubstValueError: self.entry_show(e) def entry_menu(self, time = None): "Displays the entry menu" self.__close_popups() if self.__require_file() == False: return if self.entrymenu == None: return x, y = self.__get_popup_offset(self.entrymenu) self.entrymenu.show_all() self.entrymenu.popup(None, None, lambda d: (x, y, False), 1, time) def entry_search(self, term, focusafter = False): "Searches for an entry" self.__close_popups() if term.strip() == "": return if self.__require_file() == False: return matches = [ self.entrystore.get_entry(iter) for iter in self.entrysearch.find_all(term) ] if len(matches) == 0: self.__focus_entry() self.__flash_entry() self.entry.select_region(0, -1) elif len(matches) == 1: self.entry_show(matches[0], True) else: self.popup_entrylist = EntryListPopup(matches) self.popup_entrylist.connect("entry-chosen", lambda w,e: self.entry_show(e, focusafter)) if focusafter == True: self.popup_entrylist.connect("closed", lambda w: self.__focus_entry()) self.popup_entrylist.realize() x, y = self.__get_popup_offset(self.popup_entrylist) self.popup_entrylist.show(x, y) def entry_show(self, e, focusafter = False): "Shows an entry" self.__close_popups() self.popup_entryview = EntryViewPopup(e, self.config, self.clipboard) if focusafter == True: self.popup_entryview.connect("closed", lambda w: self.__focus_entry()) def cb_goto(widget): if self.__launcher_valid(e): self.entry_goto(e) self.popup_entryview.close() self.popup_entryview.button_goto.connect("clicked", cb_goto) self.popup_entryview.button_goto.set_sensitive(self.__launcher_valid(e)) self.popup_entryview.realize() x, y = self.__get_popup_offset(self.popup_entryview) self.popup_entryview.show(x, y) def file_close(self): "Closes the current data file" self.__close_popups() self.locktimer.stop() self.datafile.close() self.entrystore.clear() self.entrymenu = None def file_open(self, file, password = None): "Opens a data file" try: return self.__file_load(file, password) except dialog.CancelError: pass except datahandler.FormatError: dialog.Error(None, _('Invalid file format'), _('The file \'%s\' contains invalid data.') % file).run() except ( datahandler.DataError, entry.EntryTypeError, entry.EntryFieldError ): dialog.Error(None, _('Unknown data'), _('The file \'%s\' contains unknown data. It may have been created by a more recent version of Revelation.') % file).run() except datahandler.PasswordError: dialog.Error(None, _('Incorrect password'), _('You entered an incorrect password for the file \'%s\', please try again.') % file).run() self.file_open(file, None) except datahandler.VersionError: dialog.Error(None, _('Unknown data version'), _('The file \'%s\' has a future version number, please upgrade Revelation to open it.') % file).run() except IOError: dialog.Error(None, _('Unable to open file'), _('The file \'%s\' could not be opened. Make sure that the file exists, and that you have permissions to open it.') % file).run() return False def file_reload(self): "Reloads the current data file" if self.datafile.get_file() == None: return self.file_open(self.datafile.get_file(), self.datafile.get_password()) def prefs(self): "Displays the preference dialog" dialog.run_unique(Preferences, None, self.config) class About(dialog.About): "About dialog" def __init__(self, parent): dialog.About.__init__(self, parent) self.set_name(_('Revelation Account Search')) self.set_comments(_('"%s"\n\nAn applet for searching and browsing a Revelation account database') % config.RELNAME) class EntryListPopup(dialog.Popup): "A popup for displaying a list of entries" def __init__(self, entries): dialog.Popup.__init__(self) self.set_default_size(225, 200) self.entrystore = data.EntryStore() self.entrystore.set_sort_column_id(0, gtk.SORT_ASCENDING) for e in entries: self.entrystore.add_entry(e) self.treeview = ui.EntryTree(self.entrystore) self.treeview.set_cursor((0,)) self.treeview.connect("row-activated", self.__cb_row_activated) self.scrolledwindow = ui.ScrolledWindow(self.treeview) self.scrolledwindow.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) self.add(self.scrolledwindow) def __cb_row_activated(self, widget, path, data = None): "Callback for tree row activation" iter = self.entrystore.get_iter(path) e = self.entrystore.get_entry(iter) self.emit("entry-chosen", e) self.close() gobject.signal_new("entry-chosen", EntryListPopup, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, ( gobject.TYPE_PYOBJECT, )) class EntryViewPopup(dialog.Popup): "A popup for displaying an entry" def __init__(self, e, cfg = None, clipboard = None): dialog.Popup.__init__(self) self.set_title(e.name) self.entryview = ui.EntryView(cfg, clipboard) self.entryview.set_border_width(0) self.entryview.display_entry(e) self.button_close = ui.Button(gtk.STOCK_CLOSE, lambda w: self.close()) self.button_goto = ui.Button(ui.STOCK_GOTO) self.buttonbox = ui.HButtonBox(self.button_goto, self.button_close) self.vbox = ui.VBox(self.entryview, self.buttonbox) self.vbox.set_border_width(12) self.vbox.set_spacing(15) self.add(self.vbox) self.connect("show", lambda w: self.button_close.grab_focus()) class Preferences(dialog.Utility): "A preference dialog" def __init__(self, parent, cfg): dialog.Utility.__init__(self, parent, "Preferences") self.config = cfg self.set_modal(False) self.notebook = ui.Notebook() self.vbox.pack_start(self.notebook) self.page_general = self.notebook.create_page(_('General')) self.__init_section_file(self.page_general) self.__init_section_menuaction(self.page_general) self.__init_section_misc(self.page_general) self.page_goto = self.notebook.create_page(_('Goto Commands')) self.__init_section_gotocmd(self.page_goto) self.connect("response", lambda w,d: self.destroy()) def __init_section_file(self, page): "Sets up a file section in a page" self.section_file = page.add_section(_('File Handling')) # entry for file self.button_file = ui.FileButton(_('Select File to Use')) ui.config_bind(self.config, "file", self.button_file) eventbox = ui.EventBox(self.button_file) self.tooltips.set_tip(eventbox, _('The data file to search for accounts in')) self.section_file.append_widget(_('File to use'), eventbox) # check-button for autolock self.check_autolock = ui.CheckButton(_('Lock file when inactive for')) ui.config_bind(self.config, "autolock", self.check_autolock) self.check_autolock.connect("toggled", lambda w: self.spin_autolock_timeout.set_sensitive(w.get_active())) self.tooltips.set_tip(self.check_autolock, _('Automatically lock the file after a period of inactivity')) # spin-entry for autolock-timeout self.spin_autolock_timeout = ui.SpinEntry() self.spin_autolock_timeout.set_range(1, 120) self.spin_autolock_timeout.set_sensitive(self.check_autolock.get_active()) ui.config_bind(self.config, "autolock_timeout", self.spin_autolock_timeout) self.tooltips.set_tip(self.spin_autolock_timeout, _('The period of inactivity before locking the file, in minutes')) # container for autolock-widgets hbox = ui.HBox() hbox.set_spacing(3) hbox.pack_start(self.check_autolock) hbox.pack_start(self.spin_autolock_timeout) hbox.pack_start(ui.Label(_('minutes'))) self.section_file.append_widget(None, hbox) def __init_section_gotocmd(self, page): "Sets up the goto command section" self.section_goto = page.add_section(_('Goto Commands')) for entrytype in entry.ENTRYLIST: if entrytype == entry.FolderEntry: continue e = entrytype() widget = ui.Entry() ui.config_bind(self.config, "/apps/revelation/launcher/%s" % e.id, widget) tooltip = _('Goto command for %s accounts. The following expansion variables can be used:\n\n') % e.typename for field in e.fields: tooltip += "%%%s: %s\n" % ( field.symbol, field.name ) tooltip += "\n" tooltip += _('%%: a % sign') + "\n" tooltip += _('%?x: optional expansion variable') + "\n" tooltip += _('%(...%): optional substring expansion') self.tooltips.set_tip(widget, tooltip) self.section_goto.append_widget(e.typename, widget) def __init_section_menuaction(self, page): "Sets up a menuaction section in a page" self.section_menuaction = page.add_section(_('Menu Action')) # radio-button for show self.radio_show = ui.RadioButton(None, _('Display account info')) ui.config_bind(self.config, "menuaction", self.radio_show, "show") self.tooltips.set_tip(self.radio_show, _('Display the account information')) self.section_menuaction.append_widget(None, self.radio_show) # radio-button for goto self.radio_goto = ui.RadioButton(self.radio_show, _('Go to account, if possible')) ui.config_bind(self.config, "menuaction", self.radio_goto, "goto") self.tooltips.set_tip(self.radio_goto, _('Open the account in an external application if possible, otherwise display it')) self.section_menuaction.append_widget(None, self.radio_goto) # radio-button for copy username/password self.radio_copy = ui.RadioButton(self.radio_show, _('Copy password to clipboard')) ui.config_bind(self.config, "menuaction", self.radio_copy, "copy") self.tooltips.set_tip(self.radio_copy, _('Copy the account password to the clipboard')) self.section_menuaction.append_widget(None, self.radio_copy) def __init_section_misc(self, page): "Sets up the misc section" self.section_misc = page.add_section(_('Miscellaneous')) # show searchentry checkbutton self.check_show_searchentry = ui.CheckButton(_('Show search entry')) ui.config_bind(self.config, "show_searchentry", self.check_show_searchentry) self.tooltips.set_tip(self.check_show_searchentry, _('Display an entry box in the applet for searching')) self.section_misc.append_widget(None, self.check_show_searchentry) # show passwords checkbutton self.check_show_passwords = ui.CheckButton(_('Show passwords and other secrets')) ui.config_bind(self.config, "show_passwords", self.check_show_passwords) self.tooltips.set_tip(self.check_show_passwords, _('Display passwords and other secrets, such as PIN codes (otherwise, hide with ******)')) self.section_misc.append_widget(None, self.check_show_passwords) # check-button for username self.check_chain_username = ui.CheckButton(_('Also copy username when copying password')) ui.config_bind(self.config, "chain_username", self.check_chain_username) self.tooltips.set_tip(self.check_chain_username, _('When the password is copied to clipboard, put the username before the password as a clipboard "chain"')) self.section_misc.append_widget(None, self.check_chain_username) def run(self): "Runs the dialog" self.show_all() def factory(applet, iid): "Applet factory function" RevelationApplet(applet, iid) return True if __name__ == "__main__": gnome.init(config.APPNAME, config.VERSION) gnomeapplet.bonobo_factory( "OAFIID:GNOME_RevelationApplet_Factory", gnomeapplet.Applet.__gtype__, config.APPNAME, config.VERSION, factory )