#!/usr/bin/env python # # Revelation 0.4.0 - a password manager for GNOME 2 # http://oss.codepoet.no/revelation/ # $Id$ # # Copyright (c) 2003-2005 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 gnome, gtk, gtk.gdk, os, pwd, sys if "@pythondir@" not in sys.path: sys.path.insert(0, "@pythondir@") from revelation import authmanager, config, data, datahandler, dialog, entry, io, ui, util class Revelation(ui.App): "The Revelation application" def __init__(self): sys.excepthook = self.__cb_exception os.umask(0077) self.program = gnome.init( config.APPNAME, config.APPNAME, gnome.libgnome_module_info_get(), sys.argv, [] ) authmanager.gnome_authentication_manager_init() ui.App.__init__(self, config.APPNAME) self.connect("delete-event", lambda w,d: True ^ self.quit()) try: self.__init_facilities() self.__init_ui() self.__init_states() except IOError: dialog.Error(self, "Missing data files", "Some of Revelations system files could not be found, please reinstall Revelation.").run() sys.exit(1) except config.ConfigError: dialog.Error(self, "Missing configuration data", "Revelation could not find its configuration data, please reinstall Revelation.").run() sys.exit(1) except ui.DataError: dialog.Error(self, "Invalid data files", "Some of Revelations system files contain invalid data, please reinstall Revelation.").run() sys.exit(1) def __init_facilities(self): "Sets up various facilities" self.clipboard = data.Clipboard() self.config = config.Config() self.datafile = io.DataFile(datahandler.Revelation) self.entryclipboard = data.EntryClipboard() self.entrystore = data.EntryStore() self.entrysearch = data.EntrySearch(self.entrystore) self.items = ui.ItemFactory(self) self.sessionclient = gnome.ui.master_client() self.undoqueue = data.UndoQueue() self.datafile.connect("changed", lambda w,f: self.__state_file(f)) self.entryclipboard.connect("content-toggled", lambda w,d: self.__state_clipboard(d)) self.sessionclient.connect("die", lambda w: self.quit()) self.sessionclient.connect("save-yourself", self.__cb_session_save) self.undoqueue.connect("changed", lambda w: self.__state_undo(self.undoqueue.get_undo_action(), self.undoqueue.get_redo_action())) self.config.monitor("search/folders", lambda k,v,d: setattr(self.entrysearch, "folders", v)) self.config.monitor("search/namedesc", lambda k,v,d: setattr(self.entrysearch, "namedesconly", v)) self.config.monitor("search/casesens", lambda k,v,d: setattr(self.entrysearch, "casesensitive", v)) def __init_states(self): "Sets the initial application state" # set window states self.set_default_size( self.config.get("view/window-width"), self.config.get("view/window-height") ) self.move( self.config.get("view/window-position-x"), self.config.get("view/window-position-y") ) self.hpaned.set_position( self.config.get("view/pane-position") ) # bind ui widgets to config keys bind = { "view/passwords" : "/menubar/menu-view/view-passwords", "view/searchbar" : "/menubar/menu-view/view-searchbar", "view/statusbar" : "/menubar/menu-view/view-statusbar", "view/toolbar" : "/menubar/menu-view/view-toolbar" } for key, path in bind.items(): ui.config_bind(self.config, key, self.uimanager.get_widget(path)) self.entryview.display_info() self.show_all() # set some variables self.entrysearch.string = "" self.entrysearch.type = None # set ui widget states self.__state_clipboard(self.entryclipboard.has_contents()) self.__state_entry([]) self.__state_file(None) self.__state_find(self.entrysearch.string) self.__state_undo(None, None) # set states from config self.config.monitor("view/searchbar", self.__cb_config_toolbar, self.searchbar) self.config.monitor("view/statusbar", self.__cb_config_toolbar, self.statusbar) self.config.monitor("view/toolbar", self.__cb_config_toolbar, self.toolbar) def __init_ui(self): "Sets up the UI" # set window icons gtk.window_set_default_icon_list( self.items.load_icon("revelation", 48), self.items.load_icon("revelation", 32), self.items.load_icon("revelation", 24), self.items.load_icon("revelation", 16) ) # load UI definitions self.uimanager.add_actions_from_file(config.DIR_UI + "/actions.xml") self.uimanager.add_ui_from_file(config.DIR_UI + "/menubar.xml") self.uimanager.add_ui_from_file(config.DIR_UI + "/popup-tree.xml") self.uimanager.add_ui_from_file(config.DIR_UI + "/toolbar.xml") # set up callbacks for actions callbacks = { "clip-chain" : lambda w: self.clip_chain(self.entrystore.get_entry(self.tree.get_active())), "clip-copy" : lambda w: self.clip_copy(self.tree.get_selected()), "clip-cut" : lambda w: self.clip_cut(self.tree.get_selected()), "clip-paste" : lambda w: self.clip_paste(self.entryclipboard.get(), self.tree.get_active()), "entry-add" : lambda w: self.entry_add(None, self.tree.get_active()), "entry-edit" : lambda w: self.entry_edit(self.tree.get_active()), "entry-goto" : lambda w: self.entry_goto(self.tree.get_selected()), "entry-remove" : lambda w: self.entry_remove(self.tree.get_selected()), "file-change-password" : lambda w: self.file_change_password(), "file-close" : lambda w: self.quit(), "file-export" : lambda w: self.file_export(), "file-import" : lambda w: self.file_import(), "file-lock" : lambda w: self.file_lock(), "file-new" : lambda w: self.file_new(), "file-open" : lambda w: self.file_open(), "file-save" : lambda w: self.file_save(self.datafile.get_file(), self.datafile.get_password()), "file-save-as" : lambda w: self.file_save(None, None), "find" : lambda w: self.entry_find(), "find-next" : lambda w: self.__entry_find(self, self.entrysearch.string, self.entrysearch.type, data.SEARCH_NEXT), "find-previous" : lambda w: self.__entry_find(self, self.entrysearch.string, self.entrysearch.type, data.SEARCH_PREVIOUS), "help-about" : lambda w: dialog.About(self).run(), "help-homepage" : lambda w: gnome.url_show(config.URL), "prefs" : lambda w: dialog.Preferences(self, self.config).run(), "pwgenerator" : lambda w: dialog.PasswordGenerator(self, self.config).run(), "quit" : lambda w: self.quit(), "redo" : lambda w: self.redo(), "select-all" : lambda w: self.tree.select_all(), "select-none" : lambda w: self.tree.unselect_all(), "undo" : lambda w: self.undo() } for id, callback in callbacks.items(): action = self.uimanager.get_action(id) action.connect("activate", callback) # set up toolbar and menus self.set_menus(self.uimanager.get_widget("/menubar")) self.toolbar = self.uimanager.get_widget("/toolbar") self.set_toolbar(self.toolbar) try: detachable = self.config.get("/desktop/gnome/interface/toolbar_detachable") except config.ConfigError: detachable = False self.searchbar = ui.Searchbar() self.add_toolbar(self.searchbar, "searchbar", 2, detachable) # set up main application widgets self.tree = ui.EntryTree(self.entrystore) self.scrolledwindow = ui.ScrolledWindow(self.tree) self.entryview = ui.EntryView(self.config) alignment = gtk.Alignment(0.5, 0.4, 0, 0) alignment.add(self.entryview) self.hpaned = ui.HPaned(self.scrolledwindow, alignment) self.set_contents(self.hpaned) # set up drag-and-drop self.drag_dest_set(gtk.DEST_DEFAULT_ALL, ( ( "text/uri-list", 0, 0 ), ), gtk.gdk.ACTION_COPY | gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_LINK ) self.connect("drag_data_received", self.__cb_drag_dest) self.tree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, ( ( "revelation/treerow", gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0), ), gtk.gdk.ACTION_MOVE) self.tree.enable_model_drag_dest(( ( "revelation/treerow", gtk.TARGET_SAME_APP | gtk.TARGET_SAME_WIDGET, 0), ), gtk.gdk.ACTION_MOVE) self.tree.connect("drag_data_received", self.__cb_tree_drag_received) # set up callbacks self.searchbar.button.connect("clicked", lambda w: self.__entry_find(self, self.searchbar.entry.get_text(), None)) self.tree.connect("popup", lambda w,d: self.popup(self.uimanager.get_widget("/popup-tree"), d.button, d.time)) self.tree.connect("doubleclick", self.__cb_tree_doubleclick) self.tree.connect("key-press-event", self.__cb_tree_keypress) self.tree.selection.connect("changed", lambda w: self.entryview.display_entry(self.entrystore.get_entry(self.tree.get_active()))) self.tree.selection.connect("changed", lambda w: self.__state_entry(self.tree.get_selected())) ##### STATE HANDLERS ##### def __save_state(self): "Saves the current application state" width, height = self.get_size() self.config.set("view/window-width", width) self.config.set("view/window-height", height) x, y = self.get_position() self.config.set("view/window-position-x", x) self.config.set("view/window-position-y", y) self.config.set("view/pane-position", self.hpaned.get_position()) def __state_clipboard(self, has_contents): "Sets states based on the clipboard contents" self.uimanager.get_action("clip-paste").set_property("sensitive", has_contents) def __state_entry(self, iters): "Sets states for entry-dependant ui items" # widget sensitivity based on number of entries self.uimanager.get_action_group("entry-multiple").set_sensitive(len(iters) > 0) self.uimanager.get_action_group("entry-single").set_sensitive(len(iters) == 1) self.uimanager.get_action_group("entry-optional").set_sensitive(len(iters) < 2) # goto sensitivity try: for iter in iters: e = self.entrystore.get_entry(iter) if self.config.get("launcher/%s" % e.id) not in ( "", None ): s = True break else: s = False except config.ConfigError: s = False self.uimanager.get_action_group("entry-goto").set_sensitive(s) # dynamic menu items for field copying for menupath in ( "/popup-tree", "/menubar/menu-edit"): menu = self.uimanager.get_widget(menupath) if menupath == "/menubar/menu-edit": menu = menu.get_submenu() # scan for copy action, and remove dynamic items children = menu.get_children() index_chain = children.index(self.uimanager.get_widget("%s/clip-chain" % menupath)) index_paste = children.index(self.uimanager.get_widget("%s/clip-paste" % menupath)) for i in range(index_chain + 1, index_paste): children[i].destroy() # fetch entry data if len(iters) != 1: continue e = self.entrystore.get_entry(iters[0]) secretfields = [ field for field in e.fields if (type(field) == entry.UsernameField or field.datatype == entry.DATATYPE_PASSWORD) and field.value != "" ] # create menuitems i = index_chain + 1 for field in secretfields: menuitem = ui.ImageMenuItem(gtk.STOCK_COPY, "Copy %s" % field.name) menuitem.tooltip = "Copy the %s to the clipboard" % field.name.lower() menuitem.connect("activate", self.__cb_clip_set, field.value) menuitem.connect("select", self.cb_menudesc, True) menuitem.connect("deselect", self.cb_menudesc, False) menu.insert(menuitem, i) i += 1 menu.show_all() def __state_file(self, file): "Sets states based on file" self.uimanager.get_action_group("file-exists").set_sensitive(file is not None) if file is not None: self.set_title(os.path.basename(file)) if io.file_is_local(file): os.chdir(os.path.dirname(file)) else: self.set_title("[New file]") def __state_find(self, string): "Sets states based on the current search string" self.uimanager.get_action_group("find").set_sensitive(string != "") def __state_undo(self, undoaction, redoaction): "Sets states based on undoqueue actions" if undoaction is None: s, l = False, "_Undo" else: s, l = True, "_Undo %s" % undoaction[1].lower() action = self.uimanager.get_action("undo") action.set_property("sensitive", s) action.set_property("label", l) if redoaction is None: s, l = False, "_Redo" else: s, l = True, "_Redo %s" % redoaction[1].lower() action = self.uimanager.get_action("redo") action.set_property("sensitive", s) action.set_property("label", l) ##### MISC CALLBACKS ##### def __cb_clip_set(self, widget, data): "Sets data on the clipboars" self.clipboard.set(data) def __cb_drag_dest(self, widget, context, x, y, seldata, info, time, userdata = None): "Handles file drops" if seldata.data is None: return files = [ file.strip() for file in seldata.data.split("\n") if file.strip() != "" ] if len(files) > 0: self.file_open(files[0]) 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(self, traceback).run() == True: gtk.main() else: sys.exit(1) def __cb_session_save(self, widget, phase, what, end, interaction, fast, data = None): "Handles session saves" self.sessionclient.set_current_directory(os.getcwd()) self.sessionclient.set_clone_command(sys.argv[0]) if self.datafile.get_file() == None: self.sessionclient.set_restart_command(1, [ sys.argv[0] ]) else: self.sessionclient.set_restart_command(2, [ sys.argv[0], self.datafile.get_file() ] ) self.__save_state() return True def __cb_tree_doubleclick(self, widget, data = None): "Handles doubleclicks on the tree" self.entry_goto(self.tree.get_selected()) def __cb_tree_drag_received(self, tree, context, x, y, seldata, info, time): "Callback for drag drops on the treeview" # get source and destination data sourceiters = self.entrystore.filter_parents(self.tree.get_selected()) destrow = self.tree.get_dest_row_at_pos(x, y) if destrow is None: destpath = ( self.entrystore.iter_n_children(None) - 1, ) pos = gtk.TREE_VIEW_DROP_AFTER else: destpath, pos = destrow destiter = self.entrystore.get_iter(destpath) destpath = self.entrystore.get_path(destiter) # avoid drops to current iter or descentants for sourceiter in sourceiters: sourcepath = self.entrystore.get_path(sourceiter) if self.entrystore.is_ancestor(sourceiter, destiter) == True or sourcepath == destpath: context.finish(False, False, time) return elif pos in ( gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, gtk.TREE_VIEW_DROP_BEFORE ) and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] - 1: context.finish(False, False, time) return elif pos in ( gtk.TREE_VIEW_DROP_INTO_OR_AFTER, gtk.TREE_VIEW_DROP_AFTER ) and sourcepath[:-1] == destpath[:-1] and sourcepath[-1] == destpath[-1] + 1: context.finish(False, False, time) return # move the entries if pos in ( gtk.TREE_VIEW_DROP_INTO_OR_BEFORE, gtk.TREE_VIEW_DROP_INTO_OR_AFTER): parent = destiter sibling = None elif pos == gtk.TREE_VIEW_DROP_BEFORE: parent = self.entrystore.iter_parent(destiter) sibling = destiter elif pos == gtk.TREE_VIEW_DROP_AFTER: parent = self.entrystore.iter_parent(destiter) sibpath = list(destpath) sibpath[-1] += 1 sibling = self.entrystore.get_iter(sibpath) self.entry_move(sourceiters, parent, sibling) context.finish(True, True, time) def __cb_tree_keypress(self, widget, data = None): "Handles key presses for the tree" # return if data.keyval == 65293: self.entry_edit() # insert elif data.keyval == 65379: self.entry_add() # delete elif data.keyval == 65535: self.entry_remove() ##### CONFIG CALLBACKS ##### def __cb_config_toolbar(self, config, value, toolbar): "Config callback for showing toolbars" if value == True: toolbar.show() else: toolbar.hide() #### UNDO / REDO CALLBACKS ##### def __cb_redo_add(self, name, actiondata): "Redoes an add action" path, e = actiondata parent = self.entrystore.get_iter(path[:-1]) sibling = self.entrystore.get_iter(path) iter = self.entrystore.add_entry(e, parent, sibling) self.tree.select(iter) def __cb_redo_edit(self, name, actiondata): "Redoes an edit action" path, preentry, postentry = actiondata iter = self.entrystore.get_iter(path) self.entrystore.update_entry(iter, postdata) self.tree.select(iter) def __cb_redo_import(self, name, actiondata): "Redoes an import action" paths, entrystore = actiondata self.entrystore.import_entry(entrystore, None) def __cb_redo_move(self, name, actiondata): "Redoes a move action" newiters = [] for prepath, postpath in actiondata: prepath, postpath = list(prepath), list(postpath) # adjust path if necessary if postpath[:len(prepath) - 1] == prepath[:-1]: if prepath[-1] <= postpath[len(prepath) - 1]: postpath[len(prepath) - 1] += 1 newiter = self.entrystore.move_entry( self.entrystore.get_iter(prepath), self.entrystore.get_iter(postpath[:-1]), self.entrystore.get_iter(postpath) ) newiters.append(newiter) if len(newiters) > 0: self.tree.select(newiters[0]) def __cb_redo_paste(self, name, actiondata): "Redoes a paste action" entrystore, parentpath, paths = actiondata iters = self.entrystore.import_entry(entrystore, None, self.entrystore.get_iter(parentpath)) if len(iters) > 0: self.tree.select(iters[0]) def __cb_redo_remove(self, name, actiondata): "Redoes a remove action" iters = [] for path, entrystore in actiondata: iters.append(self.entrystore.get_iter(path)) for iter in iters: self.entrystore.remove_entry(iter) self.tree.unselect_all() def __cb_undo_add(self, name, actiondata): "Undoes an add action" path, e = actiondata self.entrystore.remove_entry(self.entrystore.get_iter(path)) self.tree.unselect_all() def __cb_undo_edit(self, name, actiondata): "Undoes an edit action" path, preentry, postentry = actiondata iter = self.entrystore.get_iter(path) self.entrystore.update_entry(iter, postentry) self.tree.select(iter) def __cb_undo_import(self, name, actiondata): "Undoes an import action" paths, entrystore = actiondata iters = [ self.entrystore.get_iter(path) for path in paths ] for iter in iters: self.entrystore.remove_entry(iter) self.tree.unselect_all() def __cb_undo_move(self, name, actiondata): "Undoes a move action" actiondata = actiondata[:] actiondata.reverse() newiters = [] for prepath, postpath in actiondata: prepath, postpath = list(prepath), list(postpath) # adjust path if necessary if postpath[:-1] == prepath[:-1]: if prepath[-1] > postpath[len(prepath) - 1]: prepath[-1] += 1 newiter = self.entrystore.move_entry( self.entrystore.get_iter(postpath), self.entrystore.get_iter(prepath[:-1]), self.entrystore.get_iter(prepath) ) newiters.append(newiter) if len(newiters) > 0: self.tree.select(newiters[-1]) def __cb_undo_paste(self, name, actiondata): "Undoes a paste action" entrystore, parentpath, paths = actiondata iters = [ self.entrystore.get_iter(path) for path in paths ] for iter in iters: self.entrystore.remove_entry(iter) self.tree.unselect_all() def __cb_undo_remove(self, name, actiondata): "Undoes a remove action" iters = [] for path, entrystore in actiondata: parent = self.entrystore.get_iter(path[:-1]) sibling = self.entrystore.get_iter(path) iter = self.entrystore.import_entry(entrystore, entrystore.iter_nth_child(None, 0), parent, sibling) iters.append(iter) self.tree.select(iters[0]) ##### PRIVATE METHODS ##### def __entry_find(self, parent, string, entrytype, direction = data.SEARCH_NEXT): "Searches for an entry" match = self.entrysearch.find(string, entrytype, self.tree.get_active(), direction) self.entrysearch.string = string self.entrysearch.type = entrytype self.__state_find(string) if match is not None: self.tree.select(match) else: dialog.Error(parent, "No match found", "The string you searched for did not match any entries. Try searching for a different phrase.").run() def __file_autosave(self): "Autosaves the current file if needed" if self.datafile.get_file() is None or self.datafile.get_password() is None: return if self.config.get("file/autosave") == False: return self.__file_save(self.datafile.get_file(), self.datafile.get_password()) self.entrystore.changed = False def __file_load(self, file, password, datafile = None): "Loads data from a data file into an entrystore" try: if datafile is None: datafile = self.datafile while 1: try: return datafile.load(file, password, lambda: dialog.PasswordOpen(self, os.path.basename(file)).run()) except datahandler.PasswordError: dialog.Error(self, "Incorrect password", "The password you entered for the file'%s' was not correct." % file).run() except datahandler.FormatError: self.statusbar.set_status("Open failed") dialog.Error(self, "Invalid file format", "The file '%s' contains invalid data." % file).run() except ( entry.EntryTypeError, entry.EntryFieldError ): self.statusbar.set_status("Open failed") dialog.Error(self, "Unknown data", "The file '%s' contains unknown data. It may have been created by a more recent version of Revelation.." % file).run() except datahandler.VersionError: self.statusbar.set_status("Open failed") dialog.Error(self, "Unknown data version", "The file '%s' has a future version number, please upgrade Revelation to open it." % file).run() except datahandler.DetectError: self.statusbar.set_status("Open failed") dialog.Error(self, "Unable to detect filetype", "The file type of the file '%s' could not be automatically detected. Try specifying the file type manually." % file).run() except IOError: self.statusbar.set_status("Open failed") dialog.Error(self, "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() def __file_save(self, file, password, datafile = None): "Saves data to a file" try: if datafile is None: datafile = self.datafile if io.file_normpath(str(datafile)) != io.file_normpath(file) and io.file_exists(file): dialog.FileOverwrite(self, file).run() if datafile.get_handler().encryption == True: if password is None: password = dialog.PasswordSave(self, file).run() else: dialog.FileSaveInsecure(self).run() datafile.save(self.entrystore, file, password) except IOError: revelation.dialog.Error(self, "Unable to write to file", "The file '%s' could not be opened for writing. Make sure that you have the proper permissions to write to it." % file).run() self.statusbar.set_status("Save failed") def __get_common_usernames(self, e = None): "Returns a list of possibly relevant usernames" list = [] if e is not None and e.has_field(entry.UsernameField): list.append(e[entry.UsernameField]) list.append(pwd.getpwuid(os.getuid())[0]) list.extend(self.entrystore.get_popular_values(entry.UsernameField, 3)) list = {}.fromkeys(list).keys() list.sort() return list def __save_changes(self, d): "Asks the user if she wants to save her changes" if self.entrystore.changed == True and d(self).run() == True: if self.file_save(self.datafile.get_file(), self.datafile.get_password()) == False: raise dialog.CancelError ##### PUBLIC METHODS ##### def clip_chain(self, e): "Copies any usernames and 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 e.has_field(entry.UsernameField) == True and e[entry.UsernameField] != "": secrets.insert(0, e[entry.UsernameField]) self.clipboard.set(secrets) self.statusbar.set_status("Password chain copied to clipboard") def clip_copy(self, iters): "Copies entries to the clipboard" self.entryclipboard.set(self.entrystore, iters) self.statusbar.set_status("Entries copied") def clip_cut(self, iters): "Cuts entries to the clipboard" iters = self.entrystore.filter_parents(iters) self.entryclipboard.set(self.entrystore, iters) # store undo data (need paths) undoactions = [] for iter in iters: undostore = data.EntryStore() undostore.import_entry(self.entrystore, iter) path = self.entrystore.get_path(iter) undoactions.append( ( path, undostore ) ) # remove data for iter in iters: self.entrystore.remove_entry(iter) self.undoqueue.add_action( "Cut entries", self.__cb_undo_remove, self.__cb_redo_remove, undoactions ) self.__file_autosave() self.tree.unselect_all() self.statusbar.set_status("Entries cut") def clip_paste(self, entrystore, parent): "Pastes entries from the clipboard" if entrystore == None: return parent = self.tree.get_active() iters = self.entrystore.import_entry(entrystore, None, parent) paths = [ self.entrystore.get_path(iter) for iter in iters ] self.undoqueue.add_action( "Paste entries", self.__cb_undo_paste, self.__cb_redo_paste, ( entrystore, self.entrystore.get_path(parent), paths ) ) if len(iters) > 0: self.tree.select(iters[0]) self.statusbar.set_status("Entries pasted") def entry_add(self, e = None, parent = None, sibling = None): "Adds an entry" try: if e == None: d = dialog.EntryEdit(self, self.config, "Add entry") d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames()) e = d.run() iter = self.entrystore.add_entry(e, parent, sibling) self.undoqueue.add_action( "Add entry", self.__cb_undo_add, self.__cb_redo_add, ( self.entrystore.get_path(iter), e.copy() ) ) self.__file_autosave() self.tree.select(iter) self.statusbar.set_status("Entry added") except dialog.CancelError: self.statusbar.set_status("Add entry cancelled") def entry_edit(self, iter): "Edits an entry" try: if iter == None: return e = self.entrystore.get_entry(iter) d = dialog.EntryEdit(self, self.config, "Edit entry", e) d.set_fieldwidget_data(entry.UsernameField, self.__get_common_usernames(e)) if type(e) == entry.FolderEntry and self.entrystore.iter_n_children(iter) > 0: d.allow_typechange(False) n = d.run() self.entrystore.update_entry(iter, n) self.tree.select(iter) self.undoqueue.add_action( "Update entry", self.__cb_undo_edit, self.__cb_redo_edit, ( self.entrystore.get_path(iter), e.copy(), n.copy() ) ) self.__file_autosave() self.statusbar.set_status("Entry updated") except dialog.CancelError: self.statusbar.set_status("Edit entry cancelled") def entry_find(self): "Searches for an entry" d = dialog.Find(self, self.config) d.entry_string.set_text(self.entrysearch.string) d.dropdown.set_active_type(self.entrysearch.type) while 1: response = d.run() string = d.entry_string.get_text() entrytype = d.dropdown.get_active_type() if response == dialog.RESPONSE_NEXT: self.__entry_find(d, string, entrytype, data.SEARCH_NEXT) elif response == dialog.RESPONSE_PREVIOUS: self.__entry_find(d, string, entrytype, data.SEARCH_PREVIOUS) else: d.destroy() break def entry_goto(self, iters): "Goes to an entry" for iter in iters: try: # get goto data for entry e = self.entrystore.get_entry(iter) command = self.config.get("launcher/%s" % e.id) if command in ( "", None ): self.statusbar.set_status("No goto command found for " + e.typename + " entries") return subst = {} for field in e.fields: subst[field.symbol] = field.value # copy passwords to clipboar chain = [] if e.has_field(entry.UsernameField) == True and e[entry.UsernameField] != "" and "%" + entry.UsernameField.symbol not in command: chain.append(e[entry.UsernameField]) for field in e.fields: if field.datatype == entry.DATATYPE_PASSWORD and field.value != "": chain.append(field.value) self.clipboard.set(chain) # generate and run goto command command = util.parse_subst(command, subst) util.execute_child(command) self.statusbar.set_status("Entry opened") except ( util.SubstFormatError, config.ConfigError ): dialog.Error(self, "Invalid goto command format", "The goto command for '" + e.typename + "' entries is invalid, please correct this in the preferences.").run() except util.SubstValueError: dialog.Error(self, "Missing entry data", "The entry '" + e.name + "' does not have all the data required to go to it.").run() def entry_move(self, sourceiters, parent = None, sibling = None): "Moves a set of entries" if type(sourceiters) != list: sourceiters = [ sourceiters ] newiters = [] undoactions = [] for sourceiter in sourceiters: sourcepath = self.entrystore.get_path(sourceiter) newiter = self.entrystore.move_entry(sourceiter, parent, sibling) newpath = self.entrystore.get_path(newiter) undoactions.append( ( sourcepath, newpath ) ) newiters.append(newiter) self.undoqueue.add_action( "Move entry", self.__cb_undo_move, self.__cb_redo_move, undoactions ) if len(newiters) > 0: self.tree.select(newiters[0]) self.statusbar.set_status("Entries moved") def entry_remove(self, iters): "Removes the selected entries" try: if len(iters) == 0: return entries = [ self.entrystore.get_entry(iter) for iter in iters ] dialog.EntryRemove(self, entries).run() iters = self.entrystore.filter_parents(iters) # store undo data (need paths) undoactions = [] for iter in iters: undostore = data.EntryStore() undostore.import_entry(self.entrystore, iter) path = self.entrystore.get_path(iter) undoactions.append( ( path, undostore ) ) # remove data for iter in iters: self.entrystore.remove_entry(iter) self.undoqueue.add_action( "Remove entry", self.__cb_undo_remove, self.__cb_redo_remove, undoactions ) self.tree.unselect_all() self.__file_autosave() self.statusbar.set_status("Entries removed") except dialog.CancelError: self.statusbar.set_status("Entry removal cancelled") def file_change_password(self, password = None): "Changes the password of the current data file" try: if password == None: password = dialog.PasswordChange(self, self.datafile.get_password()).run() self.datafile.set_password(password) self.entrystore.changed = True self.__file_autosave() self.statusbar.set_status("Password changed") except dialog.CancelError: self.statusbar.set_status("Password change cancelled") def file_export(self): "Exports data to a foreign file format" try: file, handler = dialog.ExportFileSelector(self).run() datafile = io.DataFile(handler) self.__file_save(file, None, datafile) self.statusbar.set_status("Data exported to %s" % datafile.get_file()) except dialog.CancelError: self.statusbar.set_status("Export cancelled") def file_import(self): "Imports data from a foreign file" try: file, handler = dialog.ImportFileSelector(self).run() datafile = io.DataFile(handler) entrystore = self.__file_load(file, None, datafile) if entrystore is not None: newiters = self.entrystore.import_entry(entrystore, None) paths = [ self.entrystore.get_path(iter) for iter in newiters ] self.undoqueue.add_action( "Import data", self.__cb_undo_import, self.__cb_redo_import, ( paths, entrystore ) ) self.statusbar.set_status("Data imported from %s" % datafile.get_file()) self.__file_autosave() except dialog.CancelError: self.statusbar.set_status("Import cancelled") def file_lock(self): "Locks the current file" password = self.datafile.get_password() if password is None: return # store current state activeiter = self.tree.get_active() oldtitle = self.get_title() # lock the file self.tree.set_model(None) self.entryview.clear() self.set_title("[Locked]") self.statusbar.set_status("File locked") dialog.PasswordLock(self, password).run() # unlock the file and restore state self.tree.set_model(self.entrystore) self.tree.select(activeiter) self.set_title(oldtitle) self.statusbar.set_status("File unlocked") def file_new(self): "Opens a new file" try: self.__save_changes(dialog.FileChangedNew) self.entrystore.clear() self.datafile.close() self.undoqueue.clear() self.statusbar.set_status("New file created") except dialog.CancelError: self.statusbar.set_status("New file cancelled") def file_open(self, file = None, password = None): "Opens a data file" try: self.__save_changes(dialog.FileChangedOpen) if file is None: file = dialog.OpenFileSelector(self).run() entrystore = self.__file_load(file, password) if entrystore is None: return self.entrystore.clear() self.entrystore.import_entry(entrystore, None) self.entrystore.changed = False self.undoqueue.clear() self.statusbar.set_status("Opened file %s" % self.datafile.get_file()) except dialog.CancelError: self.statusbar.set_status("Open cancelled") def file_save(self, file = None, password = None): "Saves data to a file" try: if file is None: file = dialog.SaveFileSelector(self).run() self.__file_save(file, password) self.entrystore.changed = False self.statusbar.set_status("Data saved to file %s" % file) return True except dialog.CancelError: self.statusbar.set_status("Save cancelled") return False def quit(self): "Quits the application" try: self.__save_changes(dialog.FileChangedQuit) self.clipboard.clear() self.entryclipboard.clear() self.__save_state() gtk.main_quit() sys.exit(0) except dialog.CancelError: self.statusbar.set_status("Quit cancelled") return False def redo(self): "Redoes the previous action" action = self.undoqueue.get_redo_action() if action is None: return self.undoqueue.redo() self.statusbar.set_status("%s redone" % action[1]) self.__file_autosave() def run(self): "Runs the application" args, argdict = self.program.get_popt_args() if len(args) > 0: file = args[0] elif self.config.get("file/autoload") == True: file = self.config.get("file/autoload_file") else: file = "" if file != "": self.file_open(io.file_normpath(file)) gtk.main() def undo(self): "Undoes the previous action" action = self.undoqueue.get_undo_action() if action is None: return self.undoqueue.undo() self.statusbar.set_status("%s undone" % action[1]) self.__file_autosave() if __name__ == "__main__": app = Revelation() app.run()