# HG changeset patch # User unknown # Date 1083015251 0 # Node ID cea4127a11c0e373fb731e097f639175c263faab # Parent 0000000000000000000000000000000000000000 initial import diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab AUTHORS --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/AUTHORS Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,4 @@ +Revelation was written by: + +Erik Grinaker + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab COPYING --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/COPYING Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab ChangeLog --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ChangeLog Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,562 @@ +Revelation changelog + +---------------[ 2004-04-06 : 0.3.0 ]--------------- + +2004-04-05 Erik Grinaker + + * fixed a bug which caused previously expanded folders to + retain the open folder icon after the file is unlocked + + * set a sane default button (cancel) for the revert + confirmation dialog + + * added a NEWS file + + * added note on restarting gconfd to the INSTALL file + +2004-03-31 Erik Grinaker + + * fixed the gconf schema installation + + * fixed a couple of bugs introduced in recent changes + +2004-03-26 Erik Grinaker + + * make setup.py register the gconf schema with the gconf daemon + +2004-03-25 Erik Grinaker + + * fixed a bug in DataStore.iter_traverse_next() which + caused it to fail when given None as input + + * the password generator now uses a balancing algorithm + which ensures a minimum amount of the different + character classes + + * doubleclicking an entry now opens the edit dialog. also + moved the edit and remove menu items to the top of the + entry popup menu, for hig compliance + + * added tooltips to preference and find dialogs + + * the find dialog can be closed by pressing Escape + + * removed the Usenet account type - old accounts are + converted to the Generic type on load + +2004-03-22 Erik Grinaker + + * fixed some bugs introduced by the recent code cleanups + + * added a File/Close menu item, for HIG compliance + + * restrict the length of entry fields in the password dialog, + to prevent passwords longer than the AES key length to be given + + * the password dialog gives the focus to the password entry + whenever it is run (after displaying an error dialog etc) + +2004-03-20 Erik Grinaker + + * code cleanups; split application ui setup and internal + mechanics into a separate class + +2004-03-18 Erik Grinaker + + * code cleanups; minor cleanups in druid module + + * code cleanups; minor cleanups in main application + +2004-03-17 Erik Grinaker + + * code cleanups; split the EntryStore class into a generic + TreeStore class (improved gtk.TreeStore) and an EntryStore + subclass which handles entry storage + + * code cleanups; cleaned up the data module + + * code cleanups; cleaned up the datafile module + +2004-03-16 Erik Grinaker + + * bumped version number to 0.3.0 + + * code cleanups; cleaned up the tree widget code + + * code cleanups; moved the EntryDropdown widget into the + widget module + + * code cleanups; moved entry information and functions into + a separate module + + * code cleanups; moved the password generator into the misc + module + + * code cleanups; rewrote the entry search algorithm (and it's + oh-so-sexy ;p) + +2004-03-15 Erik Grinaker + + * code cleanups; rewrote the password dialog and the app file + handling + + * code cleanups; cleaned up custom widget code + + * code cleanups; moved stock items and icons into a separate + module + + * code cleanups; created "misc" module, and moved some functionality + into it + + * code cleanups; rewrote the dataview code + +2004-03-14 Erik Grinaker + + * code cleanups; split out custom widgets into own module, + and set up an abstract gconf synchronization superclass for use + in various custom widgets + + * code cleanups; removed the EntryTypes class, and used + functions instead + + * code cleanups; major cleanups in dialog code + +2004-03-13 Erik Grinaker + + * added functionality for locking the data file + + * fixed a bug in the find dialog which caused it to remain open + when the window destroy button was pressed + + * added find options to gconf + + * added a password generation button to password fields in the + edit dialog + + * added "length" and "avoid ambiguous characters" options for + the password generator + +2004-03-12 Erik Grinaker + + * added an option for obscuring passwords + + * redisplay the password dialog if wrong password was given when + opening a file + + * the import and export druids will set default files for various + filetypes when available + +2004-03-11 Erik Grinaker + + * fixed a bug in the Figaro's Password Manager importer, causing + it to fail on empty fields + +2004-03-06 Erik Grinaker + + * added gconf support + + * toolbar and statusbar states are saved as gconf values + + * added initial preference dialog + + * added options for automatically loading a file on startup + + +---------------[ 2004-02-29 : 0.2.1 ]--------------- + +2004-02-29 Erik Grinaker + + * reorganized menu a bit + + * bumped version number to 0.2.1 + +2004-02-28 Erik Grinaker + + * fixed a bug in datafile saver, causing the Initial Vector + generation to overflow on certain systems + +2004-02-24 Erik Grinaker + + * the search now wraps around + + * it is now possible to search for entries even when the search + dialog is closed (using Find Next and Find Previous) + + * the search dialog will recall the previous search options + when opened + +2004-02-22 Erik Grinaker + + * umask is now 0077 by default, to make new files readable + by owner only + + * fixed a bug causing revelation.ui.Tree.select() to only work + on entries with a depth less than 2 + + * fixed a bug causing undo to be possible after reverting to + the saved file + + * fixed a bug which broke the password confirmation in the + export druid + + * added search functionality + + +---------------[ 2004-02-21 : 0.2.0 ]--------------- + +2004-02-21 Erik Grinaker + + * rewrote the druid code + + * workaround for a pygtk crasher bug (122569) in druids + + * code cleanups + +2004-02-20 Erik Grinaker + + * rewrote the datafile handler + + * bumped version to 0.2.0 + + * code cleanups + +2004-02-19 Erik Grinaker + + * code cleanups + + * added a druid for exporting data + + * added support for importing and exporting XML files + + * fixed a bug causing folders imported from FPM to have no + timestamp + + * added the data version number to the XML root node + + * file headers are now checked once a file is selected + (before asking for further input, such as passwords) + + * added file type autodetection to file import subsystem + +2004-02-17 Erik Grinaker + + * removed the Wimp importer, as Wimp seems to be horribly + broken + + * added a druid for importing data + +2004-02-14 Erik Grinaker + + * added support for exporting data in Figaro's Password Manager + format + + * added support for importing other Revelation data files + + * added a hostname field to the generic account type + + * added importer for Wimp? (Where Is My Password?) data files + +2004-02-10 Erik Grinaker + + * major rewrite of file handling - it now provides import/export + infrastructure + + * support for importing data from Figaros Password Manager + + * fixed a bug causing Undo and Redo menu items to lose their + access keys + + * added Select All and Deselect All menu items + + * fixed a bug causing the open folder icon to remain after the + entrys parent was collapsed + + * added Domain field to Shell account type + + * added Crypto Key account type + + * added tooltips to input fields in the edit entry dialog + +2004-02-07 Erik Grinaker + + * bugfix; the data view didn't display time since update in minutes + + * new, more secure data file format + +2004-02-06 Erik Grinaker + + * code cleanups + + * added menu item for reverting to saved file + + * added a database field to database accounts + + * fixed a bug causing the data view to not display "1 month + since update" until after 1 month and 1 day + + * more robust error handling in file import/export + +2004-02-05 Erik Grinaker + + * implemented initial import/export infrastructure + (currently only supports Revelation data files :)) + + +---------------[ 2004-02-04 : 0.1.2 ]--------------- + +2004-02-04 Erik Grinaker + + * more code cleanups + + * bumped version number to 0.1.2 + + * workaround for possible pygtk / gtk+ bug + +2004-02-03 Erik Grinaker + + * the dataview may now be shrunk + + * major code cleanups + +2004-02-02 Erik Grinaker + + * human-readable period since update is displayed in dataview + +2004-02-01 Erik Grinaker + + * multiple entries may now be selected and changed simultaneously + +2004-01-31 Erik Grinaker + + * performance tweaks for expand/collapse row, open file and + save file + + +---------------[ 2004-01-28 : 0.1.1 ]--------------- + +2004-01-28 Erik Grinaker + + * minor HIG fixes + +2004-01-27 Erik Grinaker + + * added undo/redo functionality + + * code cleanups + + * all data is now properly escaped when needed. fixes a bug which + caused invalid xml data to be generated when saving. + +2004-01-25 Erik Grinaker + + * major code cleanups + +2004-01-23 Erik Grinaker + + * minor code cleanups + +2004-01-22 Erik Grinaker + + * all menu items now show a description in the statusbar + +2004-01-21 Erik Grinaker + + * the toolbar can be shown/hidden through the menu + + * the popup menu always allows add entry and paste, to make + it consistent with the app menu + + * code cleanups + + * HIG fixes + + * added requirements to INSTALL file + + * finalized cut/paste functionality + + * the edit dialog now recalls field values when changing type + + * new entries are placed right after the selected one when it + is not a folder + +2004-01-20 Erik Grinaker + + * empty fields are not displayed in the account view + + * added TODO file + + * many HIG fixes + + * added initial cut/paste functionality + + * removed move entry functionality, cut/paste should be used instead + + * the statusbar can be shown/hidden through the menu + +2004-01-19 Erik Grinaker + + * fixed a bug causing revelation to crash when given a relative + path as an argument + + +---------------[ 2004-01-18 : 0.1.0 ]--------------- + +2004-01-18 Erik Grinaker + + * bugfixes + +2004-01-12 Erik Grinaker + + * a few minor bugfixes + +2004-01-10 Erik Grinaker + + * more code cleanups + +2004-01-09 Erik Grinaker + + * many code cleanups and minor changes + +2004-01-06 Erik Grinaker + + * asks user for overwrite confirmation if saving to existing + file + + * added revelation.desktop file + +2004-01-05 Erik Grinaker + + * if a command-line argument is given on start, it is assumed + to be a file and will be opened + +2004-01-04 Erik Grinaker + + * an entry is now shown and selected when added + + * all children of a folder are collapsed when it is collapsed + + * added edit menu with add, edit and remove entry items + + * menu and toolbar item sensitivity is updated based on + current entry type + + * entry type can no longer be changed when the entry is a + folder with children + + * folders get closed-icon when all children have been removed + or moved + + * added shortcuts for add, edit and remove in the tree + + * misc widgets are made (in)sensitive based on the current type + + * cleaned up popup menu code + + * finalized the move entry functionality + + * ui changes to about dialog, and added home page menu iter + +2003-12-27 Erik Grinaker + + * display open folder icon when folder treerow is expanded + + * added functionality for moving entries + +2003-12-26 Erik Grinaker + + * added setup.py, to be used instead of Makefile + + * bumped version number to 0.1.0 + + * changed the icon fs layout, and replaced some icons with + lower-resolution ones which look nicer + +2003-12-23 Erik Grinaker + + * more code cleanups (again) + +2003-12-22 Erik Grinaker + + * even more code cleanups and minor changes + +2003-12-21 Erik Grinaker + + * lots of code cleanups + +2003-12-20 Erik Grinaker + + * moved all app code from lib to main script + + * lots of code cleanups and minor changes + + * renamed category entry type to folder + +2003-12-16 Erik Grinaker + + * the file selector now remembers the previous directory + + * now asks for password confirmation when using save as + + * set version number to 0.0.1, as the first version will be + rather primitive + + * added a generic account type + + * display account type + +2003-12-15 Erik Grinaker + + * added database account type + +2003-12-03 Erik Grinaker + + * the name of the current file is displayed in the window title + + * doubleclick and space expands/collapses categories, and + return opens the edit dialog + +2003-11-30 Erik Grinaker + + * removed the protocol field from email account type + + * new passwords must be confirmed, and the current password + must be entered when changing it + +2003-11-19 Erik Grinaker + + * misc designtweaks + + * application info is only cleared when displaying an entry + + * the password for the current file can now be changed + +2003-11-17 Erik Grinaker + + * password is no longer SHA1 hashed + + * data files no longer have a magic string at the start (it's + only AES data now) + + * redesigned the "edit entry" dialog + +2003-11-16 Erik Grinaker + + * initial version + + * added new, open and save to toolbar + + * only install .png files from pixmaps/ (to make it install + correctly when using CVS) + + * new popup menu containing only "new entry" when right-clicking + but no entry + + * xml importer now adds empty fields according to entry type if + the field is missing in the data file + + * added ccv2 number to credit card entry type + + * beautified dialogs + + * renamed "LDAP directory" account to "LDAP" + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab INSTALL --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/INSTALL Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,17 @@ +To use Revelation you need to have pygtk 2.0.0, gnome-python 2.0.0 +and pycrypto 1.9 installed. + +Installation is done using a standard Python setup.py file. Quick +instructions are (as root): + +python setup.py build +python setup.py install + + +You may need to restart gconfd-2 after installing for the first time, +as gconf doesn't sense changes to the central database. Run +'gconftool-2 --shutdown' as your normal user, or send it a SIGHUP. + +If you are reinstalling or upgrading Revelation you may need to use +the --force argument to install, otherwise the old files will not be +replaced. diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab NEWS --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/NEWS Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,21 @@ +2004-04-06: Revelation 0.3.0 +============================ + +New features: +- integrated password generator +- gconf integration +- new preference dialog +- option for automatically opening file on startup +- option for hiding passwords +- an opened file can be locked +- many small ui and usability improvements + +Bugfixes: +- the Figaro's Password Manager importer failed on empty fields +- the password dialog allowed passwords longer than the keylength + of the encryption algorithms, causing them to fail + +Other changes: +- complete rewrite of some components, and major cleanups of others +- removed the Usenet account type + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,5 @@ +Revelation is a password manager for the GNOME 2 desktop. It organizes +accounts in a tree structure, and stores them as AES-encrypted XML. + +The project website is located at http://oss.wired-networks.net/revelation/ + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab TODO --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/TODO Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,48 @@ +0.3.x: +- password strength check (cracklib?) +- use a combo box for the username field +- use a fileentry widget for file fields (key file, certificate) +- use a spin button for integer fields (port) +- add a dropdown widget for list fields (like email protocol etc) +- option for autolocking the file after a period of inactivity +- don't use modal dialogs unless absolutely necessary +- add a password generator dialog (under Tools/Password Generator menu) +- add info about file as cmdline-arg in --help text +- create an Entry object, to replace the data dict which is passed + around currently +- string cleanups +- program-launchers for misc account types +- import/export of .netrc files +- bug; "save as" is called when choosing save in "confirm quit" dialog + +0.4.x: +- remove support for version 0 data files +- improve the file format +- will introduce a gnome 2.6 dependency +- gnome integration (gnome-vfs, session management, recent docs etc) +- add support for "copy username/password to clipboard", useful when + hiding the passwords +- drag'n'drop +- display filtering +- new gtk+ 2.4 file selector +- add a mime-type for the data file (application/x-revelation-data) + - based on the fd.o shared mime database spec +- use themed icons when possible +- somehow integrate with gnome-keyring (import/export?) +- make the EntryDropdown display the icon on the button as well as + in the menu + +0.5.x: +- gnome panel applets (account lookup, password generator etc) +- accessibility improvements +- help / documentation +- internationalization +- translations + +1.0.x: +- attempt full HIG compliance +- misc polishing + +1.1.x: +- allow users to create custom account types + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab gnome/revelation.desktop --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gnome/revelation.desktop Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Revelation Password Manager +GenericName=Password Manager +Version=0.3.0 +Comment=Organize and secure your passwords +Exec=revelation +Icon=revelation.png +Terminal=false +Type=Application +Categories=GNOME;Application;Office; +StartupNotify=true + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab gnome/revelation.schemas --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gnome/revelation.schemas Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,170 @@ + + + + + /schemas/apps/revelation/file/autoload + /apps/revelation/file/autoload + revelation + bool + false + + + Autoload a file on startup + + If true, Revelation will automatically + load a data file on startup. The file to + open is specified in the "autoload_file" key. + + + + + + /schemas/apps/revelation/file/autoload_file + /apps/revelation/file/autoload_file + revelation + string + + + + A file to autoload on startup + + The full path to a file which Revelation + should autoload when starting. The "autoload" + key must also be set to true for this to + work. + + + + + + /schemas/apps/revelation/passwordgen/avoid_ambiguous + /apps/revelation/passwordgen/avoid_ambiguous + revelation + bool + true + + + Avoid ambiguous characters + + If enabled, generated passwords will not + include ambiguous characters (such as + 0 and O). + + + + + + /schemas/apps/revelation/passwordgen/length + /apps/revelation/passwordgen/length + revelation + int + 8 + + + Length of passwords + + The number of characters in generated + passwords. A length less than 4 characters + will be ignored. + + + + + + /schemas/apps/revelation/search/folders + /apps/revelation/search/folders + revelation + bool + true + + + Search for folders + + When enabled, the entry searcher will search + for folders as well as normal accounts. + + + + + + /schemas/apps/revelation/search/namedesc + /apps/revelation/search/namedesc + revelation + bool + false + + + Search in name and description only + + When enabled, the entry searcher will only + search in the name and description of + entries. + + + + + + /schemas/apps/revelation/search/casesens + /apps/revelation/search/casesens + revelation + bool + false + + + Case-sensitive search + + When enabled, searches will be case-sensitive. + + + + + + /schemas/apps/revelation/view/passwords + /apps/revelation/view/passwords + revelation + bool + true + + + Show passwords + + When true, passwords will be displayed + in plain text. Turning this option off + will obscure passwords with ******. + + + + + + /schemas/apps/revelation/view/toolbar + /apps/revelation/view/toolbar + revelation + bool + true + + + Displays the toolbar + + When set to "true", Revelation will display + its toolbar. + + + + + + /schemas/apps/revelation/view/statusbar + /apps/revelation/view/statusbar + revelation + bool + true + + + Displays the statusbar + + When set to "true", Revelation will display + its statusbar. + + + + + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-creditcard.png Binary file pixmaps/account-creditcard.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-cryptokey.png Binary file pixmaps/account-cryptokey.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-database.png Binary file pixmaps/account-database.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-door.png Binary file pixmaps/account-door.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-email.png Binary file pixmaps/account-email.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-ftp.png Binary file pixmaps/account-ftp.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-generic.png Binary file pixmaps/account-generic.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-phone.png Binary file pixmaps/account-phone.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-shell.png Binary file pixmaps/account-shell.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/account-website.png Binary file pixmaps/account-website.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/folder-open.png Binary file pixmaps/folder-open.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/folder.png Binary file pixmaps/folder.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/password.png Binary file pixmaps/password.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/revelation-16x16.png Binary file pixmaps/revelation-16x16.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab pixmaps/revelation.png Binary file pixmaps/revelation.png has changed diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +from distutils.core import setup +import sys, os + +setup( + name = 'Revelation', + version = '0.3.0', + description = 'Password manager for GNOME 2', + author = 'Erik Grinaker', + author_email = 'erikg@wired-networks.net', + url = 'http://oss.wired-networks.net/revelation/', + + packages = [ 'revelation' ], + package_dir = { 'revelation' : 'src/lib' }, + + scripts = [ 'src/revelation' ], + + data_files = [ + ( 'share/pixmaps', [ + 'pixmaps/revelation.png' + ] ), + + ( 'share/revelation/pixmaps', [ + 'pixmaps/account-creditcard.png', + 'pixmaps/account-cryptokey.png', + 'pixmaps/account-database.png', + 'pixmaps/account-door.png', + 'pixmaps/account-email.png', + 'pixmaps/account-ftp.png', + 'pixmaps/account-generic.png', + 'pixmaps/account-phone.png', + 'pixmaps/account-shell.png', + 'pixmaps/account-website.png', + 'pixmaps/folder.png', + 'pixmaps/folder-open.png', + 'pixmaps/password.png', + 'pixmaps/revelation.png', + 'pixmaps/revelation-16x16.png' + ] ), + + ( 'share/applications', [ + 'gnome/revelation.desktop' + ] ), + + ( '/etc/gconf/schemas', [ + 'gnome/revelation.schemas' + ] ) + ] +) + +if "install" in sys.argv: + p = os.popen("GCONF_CONFIG_SOURCE=`gconftool-2 --get-default-source` gconftool-2 --makefile-install-rule /etc/gconf/schemas/revelation.schemas") + p.close() + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/__init__.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,51 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Library initialization script +# +# +# Copyright (c) 2003-2004 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 misc, stock, entry, widget, ui, data, datafile, dialog, druid, sys + +APPNAME = "Revelation" +VERSION = "0.3.0" +DATAVERSION = 1 +RELNAME = "When the mind rests on nothing, true mind appears" +URL = "http://oss.wired-networks.net/revelation/" +AUTHOR = "Erik Grinaker " +COPYRIGHT = "Copyright \302\251 2003-2004 Erik Grinaker" + +PREFIX = sys.prefix +DATADIR = PREFIX + "/share/revelation" + +# set up some exceptions +class Error(Exception): + """Base class for errors""" + pass + +class CancelError(Error): + """Exception for user cancellation""" + pass + +class FileError(Error): + """Exception for file errors""" + pass + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/data.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/data.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,487 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module containing data-related functionality +# +# +# Copyright (c) 2003-2004 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 gtk, revelation, gobject + + +ENTRYSTORE_COL_NAME = 0 +ENTRYSTORE_COL_ICON = 1 +ENTRYSTORE_COL_TYPE = 2 +ENTRYSTORE_COL_DESC = 3 +ENTRYSTORE_COL_UPDATED = 4 +ENTRYSTORE_COL_FIELDS = 5 + +UNDO = "undo" +REDO = "redo" + +SEARCH_NEXT = "next" +SEARCH_PREV = "prev" + + +class EntrySearch(gobject.GObject): + + def __init__(self, entrystore): + gobject.GObject.__init__(self) + self.entrystore = entrystore + + self.string = "" + self.type = None + self.folders = gtk.TRUE + self.namedesc = gtk.FALSE + self.casesens = gtk.FALSE + + + def __setattr__(self, name, value): + if name == "string": + self.emit("string_changed", value) + + gobject.GObject.__setattr__(self, name, value) + + + def find(self, offset, direction = SEARCH_NEXT): + if direction == SEARCH_NEXT: + traverse = self.entrystore.iter_traverse_next + else: + traverse = self.entrystore.iter_traverse_prev + + iter = traverse(offset) + + while not self.entrystore.iter_compare(iter, offset): + if self.match(iter) == gtk.TRUE: + return iter + + iter = traverse(iter) + + return None + + + def match(self, iter): + if iter == None: + return gtk.FALSE + + data = self.entrystore.get_entry(iter) + + # check type + if data["type"] == revelation.entry.ENTRY_FOLDER and self.folders == gtk.FALSE: + return gtk.FALSE + + if self.type is not None and data["type"] not in [ self.type, revelation.entry.ENTRY_FOLDER ]: + return gtk.FALSE + + # check the items + items = [ data["name"], data["description"] ] + if self.namedesc == gtk.FALSE: + items.extend(data["fields"].values()) + + # run the search + for item in items: + if self.casesens == gtk.TRUE and item.find(self.string) >= 0: + return gtk.TRUE + elif self.casesens == gtk.FALSE and item.lower().find(self.string.lower()) >= 0: + return gtk.TRUE + + return gtk.FALSE + + +gobject.signal_new("string_changed", EntrySearch, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_STRING, )) + + + +class TreeStore(gtk.TreeStore): + + def filter_parents(self, iters): + parents = [] + for child in iters: + for parent in iters: + if self.is_ancestor(parent, child): + break + else: + parents.append(child) + + return parents + + + def get_iter(self, path): + if path in [ None, "", () ]: + return None + + try: iter = gtk.TreeStore.get_iter(self, path) + except ValueError: iter = None + + return iter + + + def get_path(self, iter): + if iter is None: + return None + else: + return gtk.TreeStore.get_path(self, iter) + + + def has_contents(self): + return self.iter_n_children(None) > 0 + + + def is_empty(self): + return self.iter_n_children(None) == 0 + + + def iter_compare(self, iter1, iter2): + return self.get_path(iter1) == self.get_path(iter2) + + + def iter_traverse_next(self, iter): + + # get the first child, if any + child = self.iter_nth_child(iter, 0) + if child is not None: + return child + + # check for a sibling or, if not found, a sibling of any ancestors + parent = iter + while parent is not None: + sibling = parent.copy() + sibling = self.iter_next(sibling) + + if sibling is not None: + return sibling + + parent = self.iter_parent(parent) + + return None + + + def iter_traverse_prev(self, iter): + + # get the previous sibling, or parent, of the iter - if any + if iter is not None: + parent = self.iter_parent(iter) + index = self.get_path(iter)[-1] + + # if no sibling is found, return the parent + if index == 0: + return parent + + # otherwise, get the sibling + iter = self.iter_nth_child(parent, index - 1) + + # get the last, deepest child of the sibling or root, if any + while self.iter_n_children(iter) > 0: + iter = self.iter_nth_child(iter, self.iter_n_children(iter) - 1) + + return iter + + + +class EntryStore(TreeStore): + + def __init__(self): + TreeStore.__init__(self, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_PYOBJECT) + + + def add_entry(self, parent, data = None): + iter = self.append(parent) + self.update_entry(iter, data) + return iter + + + def export_entrystore(self, iters = [], entrystore = None, parent = None): + if entrystore == None: + entrystore = EntryStore() + + for iter in iters: + child = entrystore.add_entry(parent, self.get_entry(iter)) + + for i in range(self.iter_n_children(iter)): + self.export_entrystore([self.iter_nth_child(iter, i)], entrystore, child) + + return entrystore + + + def get_entry(self, iter): + if iter == None: + return None + + data = { + "name" : self.get_value(iter, ENTRYSTORE_COL_NAME), + "type" : self.get_value(iter, ENTRYSTORE_COL_TYPE), + "icon" : self.get_value(iter, ENTRYSTORE_COL_ICON), + "description" : self.get_value(iter, ENTRYSTORE_COL_DESC), + "updated" : self.get_value(iter, ENTRYSTORE_COL_UPDATED), + "fields" : self.get_value(iter, ENTRYSTORE_COL_FIELDS) + } + + for field in revelation.entry.get_entry_fields(data["type"]): + if not data["fields"].has_key(field): + data["fields"][field] = "" + + return data + + + def get_entry_type(self, iter): + if iter is None: + return None + else: + return self.get_value(iter, ENTRYSTORE_COL_TYPE) + + + def import_entrystore(self, entrystore, parent = None, sibling = None, importiter = None): + baseiters = [] + + for i in range(entrystore.iter_n_children(importiter)): + importchild = entrystore.iter_nth_child(importiter, i) + + if sibling == None: + childiter = self.add_entry(parent, entrystore.get_entry(importchild)) + else: + childiter = self.insert_entry_before(parent, sibling, entrystore.get_entry(importchild)) + + baseiters.append(childiter) + + if entrystore.iter_has_child(importchild): + self.import_entrystore(entrystore, childiter, None, importchild) + + return baseiters + + + def import_entrystore_after(self, entrystore, parent, sibling): + return self.import_entrystore(entrystore, parent, self.iter_next(sibling)) + + + def import_entrystore_before(self, entrystore, parent, sibling): + return self.import_entrystore(entrystore, parent, sibling) + + + def insert_entry_after(self, parent, sibling, data = None): + iter = self.insert_after(parent, sibling) + self.update_entry(iter, data) + return iter + + + def insert_entry_before(self, parent, sibling, data = None): + iter = self.insert_before(parent, sibling) + self.update_entry(iter, data) + return iter + + + def remove_entry(self, iter): + while self.iter_has_child(iter): + self.remove_entry(self.iter_nth_child(iter, 0)) + self.remove(iter) + + + def update_entry(self, iter, data): + if data is not None: + self.set_value(iter, ENTRYSTORE_COL_NAME, data.get("name", "")) + self.set_value(iter, ENTRYSTORE_COL_TYPE, data.get("type", revelation.entry.ENTRY_FOLDER)) + self.set_value(iter, ENTRYSTORE_COL_DESC, data.get("description", "")) + self.set_value(iter, ENTRYSTORE_COL_UPDATED, data.get("updated", 0)) + self.set_value(iter, ENTRYSTORE_COL_FIELDS, data.get("fields", {})) + self.set_value(iter, ENTRYSTORE_COL_ICON, revelation.entry.get_entry_data(data.get("type", revelation.entry.ENTRY_FOLDER), "icon")) + + + +class DataStore(EntryStore): + + def __init__(self): + EntryStore.__init__(self) + self.changed = gtk.FALSE + + + def add_entry(self, parent, data = None): + + # place inside parent if folder + if self.get_entry_type(parent) in (revelation.entry.ENTRY_FOLDER, None): + iter = EntryStore.add_entry(self, parent, data) + + # place after "parent" if not folder + else: + iter = EntryStore.insert_entry_after(self, self.iter_parent(parent), parent, data) + + self.changed = gtk.TRUE + + return iter + + + def clear(self): + EntryStore.clear(self) + self.changed = gtk.FALSE + + + def get_entry(self, iter): + data = EntryStore.get_entry(self, iter) + + # always return the normal, closed folder icon for folders. + # the open folder icon is only for tree-internal use. + if data is not None and data["icon"] == revelation.stock.STOCK_FOLDER_OPEN: + data["icon"] = revelation.stock.STOCK_FOLDER + + return data + + + def remove_entry(self, iter): + parent = self.iter_parent(iter) + EntryStore.remove_entry(self, iter) + + # collapse parent if empty + if self.iter_n_children(parent) == 0: + self.set_folder_state(parent, gtk.FALSE) + + self.changed = gtk.TRUE + + + def set_folder_state(self, iter, open): + if iter == None or self.get_entry_type(iter) != revelation.entry.ENTRY_FOLDER: + return + + if open == gtk.TRUE: + icon = revelation.stock.STOCK_FOLDER_OPEN + else: + icon = revelation.stock.STOCK_FOLDER + + self.set_value(iter, ENTRYSTORE_COL_ICON, icon) + + + def update_entry(self, iter, data): + if data is None: + return + + EntryStore.update_entry(self, iter, data) + + # keep icon if current is folder-open and the type still is folder + if data["type"] == revelation.entry.ENTRY_FOLDER and self.get_value(iter, ENTRYSTORE_COL_ICON) == revelation.stock.STOCK_FOLDER_OPEN: + self.set_value(iter, ENTRYSTORE_COL_ICON, revelation.stock.STOCK_FOLDER_OPEN) + + self.changed = gtk.TRUE + + + +class EntryClipboard(EntryStore): + + def __init__(self): + EntryStore.__init__(self) + + + def copy(self, datastore, iters): + if len(iters) > 0 and None not in iters: + self.clear() + datastore.export_entrystore(iters, self) + self.emit("copy") + + + def cut(self, datastore, iters): + self.copy(datastore, iters) + + for iter in iters: + datastore.remove_entry(iter) + + self.emit("cut") + + + def paste(self, datastore, parent, sibling = None): + if sibling == None: + iters = datastore.import_entrystore(self, parent) + else: + iters = datastore.import_entrystore_after(self, parent, sibling) + + self.emit("paste") + return iters + + +gobject.signal_new("copy", EntryClipboard, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, ()) +gobject.signal_new("cut", EntryClipboard, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, ()) +gobject.signal_new("paste", EntryClipboard, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, ()) + + + +class UndoQueue(gobject.GObject): + + def __init__(self): + gobject.GObject.__init__(self) + self.queue = [] + self.actionptr = 0 + + + def add_action(self, name, action, data): + + # remove any items later in the queue + del self.queue[self.actionptr:] + + self.queue.append({ "name" : name, "action" : action, "data" : data }) + self.actionptr = len(self.queue) + + self.emit("can-undo", self.can_undo()) + self.emit("can-redo", self.can_undo(REDO)) + + + def can_undo(self, method = UNDO): + if method == UNDO: + return self.actionptr > 0 + else: + return self.actionptr < len(self.queue) + + + def clear(self): + self.queue = [] + self.actionptr = 0 + + self.emit("can-undo", gtk.FALSE) + self.emit("can-redo", gtk.FALSE) + + + def get_data(self, method = UNDO): + if method == UNDO: + ptr = self.actionptr - 1 + else: + ptr = self.actionptr + + if self.can_undo(method): + return self.queue[ptr] + else: + return None + + + def redo(self): + if self.can_undo(REDO): + self.emit("redo", self.get_data(REDO)) + self.actionptr = self.actionptr + 1 + + self.emit("can-undo", self.can_undo()) + self.emit("can-redo", self.can_undo(REDO)) + + + def undo(self): + if self.can_undo(): + self.emit("undo", self.get_data()) + self.actionptr = self.actionptr - 1 + + self.emit("can-undo", self.can_undo()) + self.emit("can-redo", self.can_undo(REDO)) + + +gobject.signal_new("undo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_PYOBJECT, )) +gobject.signal_new("redo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_PYOBJECT, )) +gobject.signal_new("can-undo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_BOOLEAN, )) +gobject.signal_new("can-redo", UndoQueue, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_BOOLEAN, )) + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/datafile.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/datafile.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,881 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module for importing from / exporting to a datafile +# +# +# Copyright (c) 2003-2004 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 gobject, os, revelation, gtk, libxml2, zlib, random, math, time, binascii, re +from Crypto.Cipher import AES, Blowfish +from Crypto.Hash import MD5 + +TYPE_AUTO = "autodetect" +TYPE_FPM = "fpm" +TYPE_REVELATION = "revelation" +TYPE_XML = "xml" + +CHECKBUFFER = 4096 + +# data classes +class FileTypes(object): + + def __init__(self): + + self.filetypes = { + TYPE_FPM : { + "name" : "Figaro's Password Manager", + "import" : gtk.TRUE, + "export" : gtk.TRUE, + "datahandler" : FPMData, + "defaultfile" : "~/.fpm/fpm" + }, + + TYPE_REVELATION : { + "name" : "Revelation", + "import" : gtk.TRUE, + "export" : gtk.FALSE, + "datahandler" : RevelationData, + "defaultfile" : None + }, + + TYPE_XML : { + "name" : "XML (eXtensible Markup Language)", + "import" : gtk.TRUE, + "export" : gtk.TRUE, + "datahandler" : RevelationXMLData, + "defaultfile" : None + } + } + + + def get_export_types(self): + types = [] + for type, data in self.filetypes.items(): + if data["export"] == gtk.TRUE: + types.append(type) + + types.sort() + return types + + + def get_import_types(self): + types = [] + for type, data in self.filetypes.items(): + if data["import"] == gtk.TRUE: + types.append(type) + + types.sort() + return types + + + def get_data(self, type, attr = None): + if attr == None: + return self.filetypes[type] + else: + return self.filetypes[type][attr] + + + def type_exists(self, type): + return self.filetypes.has_key(type) + + + + +# a few exceptions +class FormatError(Exception): + """Exception for invalid file formats""" + pass + +class PasswordError(Exception): + """Exception for wrong password""" + pass + +class VersionError(Exception): + """Exception for unknown versions""" + pass + +class DetectError(Exception): + """Exception for failed autodetection""" + pass + +class EntryError(Exception): + """Base class for entry errors""" + pass + +class EntryFieldError(EntryError): + """Exception for invalid field type for an entry type""" + pass + +class EntryTypeError(EntryError): + """Exception for unknown entry type""" + pass + + + +class DataFile(gobject.GObject): + + def __init__(self, file = None, type = None): + gobject.GObject.__init__(self) + self.filetypes = FileTypes() + self.handler = None + + self.type = type + self.file = file + + + # set up custom attribute handling, so that the data handlers can + # be accessed through the datafile instance (for setting options etc) + def __getattr__(self, name): + if name == "handler": + return None + else: + return getattr(self.handler, name) + + + def __setattr__(self, name, value): + if hasattr(self.handler, name): + self.handler.password = value + + else: + if name == "type" and value is not None: + handler = self.filetypes.get_data(value, "datahandler")() + self.handler = handler + + gobject.GObject.__setattr__(self, name, value) + + + def __read(self, file, bytes = -1): + if file == None: + raise IOError + + fp = open(file, "rb", 0) + data = fp.read(bytes) + fp.close() + + return data + + + def __write(self, file, data): + if file == None: + raise IOError + + fp = open(file, "wb", 0) + fp.write(data) + fp.flush() + fp.close() + + + def check_file(self): + if self.file == None or os.access(self.file, os.R_OK) == 0: + raise IOError + + + def check_format(self): + data = self.__read(self.file, CHECKBUFFER) + self.handler.check_data(data) + + + def detect_type(self): + header = self.__read(self.file, CHECKBUFFER) + + for detecttype in self.filetypes.get_import_types(): + handler = self.filetypes.get_data(detecttype, "datahandler")() + + if handler.detect_type(header) == gtk.TRUE: + self.type = detecttype + return detecttype + + raise DetectError + + + def load(self): + self.check_file() + self.check_format() + + entrystore = revelation.data.EntryStore() + data = self.__read(self.file) + self.handler.import_data(entrystore, data) + + return entrystore + + + def save(self, entrystore): + data = self.handler.export_data(entrystore) + self.__write(self.file, data) + + + +class Data(gobject.GObject): + + def __init__(self): + gobject.GObject.__init__(self) + + + def cipher_decrypt(self, data): + if len(data) % self.cipher.block_size != 0: + raise FormatError + + return self.cipher.decrypt(data) + + + def cipher_encrypt(self, data): + if len(data) % self.cipher.block_size != 0: + raise FormatError + + return self.cipher.encrypt(data) + + + def cipher_init(self, engine, password, iv = None, blocksize = None, keysize = None, keypad = chr(0)): + if blocksize != None: + engine.block_size = blocksize + + if keysize != None: + engine.key_size = keysize + + if len(password) > keysize: + raise PasswordError + + padlen = keysize - (len(password) % keysize) + if padlen == 32: + padlen = 0 + + password = password + (keypad * padlen) + + if iv == None: + self.cipher = engine.new(password) + else: + self.cipher = engine.new(password, engine.MODE_CBC, iv) + + + def xml_import_attrs(self, node): + attrs = {} + + attr = node.properties + while attr is not None: + attrs[attr.name] = attr.content + attr = attr.next + + return attrs + + + def xml_import_init(self, xml): + try: + doc = libxml2.parseDoc(xml) + except (libxml2.parserError, TypeError): + raise FormatError + + return doc + + + def xml_import_scan(self, node, childname): + child = node.children + while child is not None and child.name != childname: + child = child.next + + if child is not None and child.name == childname: + return child + else: + return None + + + +class FPMData(Data): + + def __init__(self): + Data.__init__(self) + self.blocksize = 8 + self.keysize = 0 + self.password = None + + + def __decrypt_field(self, data): + data = self.__hex_to_bin(data) + data = self.cipher_decrypt(data) + data = self.__unrotate_field(data) + return data + + + def __encrypt_field(self, data): + blocks = (len(data) / (self.blocksize - 1)) + 1 + size = self.blocksize * blocks + + # add noise + data = data + "\x00" + data = data + self.__generate_noise(size - len(data)) + + # rotate and encrypt field + data = self.__rotate_field(data) + data = self.cipher_encrypt(data) + + # ascii-armor data + data = self.__bin_to_hex(data) + + return data + + + def __generate_noise(self, bytes): + noise = "" + for i in range(bytes): + noise = noise + chr(int(random.random() * 255)) + + return noise + + + def __generate_salt(self): + salt = "" + for i in range(4): + salt = salt + chr(int(random.random() * 255)) + + salt = self.__bin_to_hex(salt) + return salt + + + def __bin_to_hex(self, data): + hex = "" + for i in range(len(data)): + high = ord(data[i]) / 16 + low = ord(data[i]) - high * 16 + hex = hex + chr(ord('a') + high) + chr(ord('a') + low) + + return hex + + + def __hex_to_bin(self, data): + bin = "" + for i in range(len(data) / 2): + high = ord(data[2*i]) - ord("a") + low = ord(data[2*i+1]) - ord("a") + bin = bin + chr(high*16+low) + + return bin + + + def __rotate_field(self, data): + blocks = int(math.ceil(len(data) / float(self.blocksize))) + + scrambled = "" + for block in range(blocks): + for offset in range(self.blocksize): + scrambled = scrambled + data[offset * blocks + block] + + return scrambled + + + def __unrotate_field(self, data): + blocks = int(math.ceil(len(data) / float(self.blocksize))) + plain = "" + + try: + for offset in range(self.blocksize): + for block in range(blocks): + char = data[block * self.blocksize + offset] + + if char == "\x00": + raise "done" + + plain = plain + char + + except "done": + pass + + + return plain + + + def __xml_export_convert_data(self, data): + fpmdata = { + "title" : data["name"], + "user" : "", + "url" : "", + "password" : "", + "notes" : data["description"], + "category" : "", + "launcher" : "" + } + + fields = data["fields"] + + if data["type"] == revelation.entry.ENTRY_ACCOUNT_CREDITCARD: + fpmdata["user"] = fields[revelation.entry.FIELD_CREDITCARD_CARDNUMBER] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PIN] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_CRYPTOKEY: + fpmdata["user"] = fields[revelation.entry.FIELD_GENERIC_KEYFILE] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PASSWORD] + fpmdata["url"] = fields[revelation.entry.FIELD_GENERIC_HOSTNAME] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_DATABASE: + fpmdata["user"] = fields[revelation.entry.FIELD_GENERIC_USERNAME] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PASSWORD] + fpmdata["url"] = fields[revelation.entry.FIELD_GENERIC_HOSTNAME] + ": " + fields[revelation.entry.FIELD_GENERIC_DATABASE] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_DOOR: + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_CODE] + fpmdata["url"] = fields[revelation.entry.FIELD_GENERIC_LOCATION] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_EMAIL: + fpmdata["user"] = fields[revelation.entry.FIELD_GENERIC_USERNAME] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PASSWORD] + fpmdata["url"] = fields[revelation.entry.FIELD_GENERIC_HOSTNAME] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_FTP: + fpmdata["user"] = fields[revelation.entry.FIELD_GENERIC_USERNAME] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PASSWORD] + fpmdata["url"] = "ftp://" + fields[revelation.entry.FIELD_GENERIC_HOSTNAME] + + if fields[revelation.entry.FIELD_GENERIC_PORT] != "": + fpmdata["url"] = fpmdata["url"] + ":" + fields[revelation.entry.FIELD_GENERIC_PORT] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_GENERIC: + fpmdata["user"] = fields[revelation.entry.FIELD_GENERIC_USERNAME] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PASSWORD] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_PHONE: + fpmdata["user"] = fields[revelation.entry.FIELD_PHONE_PHONENUMBER] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PIN] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_SHELL: + fpmdata["user"] = fields[revelation.entry.FIELD_GENERIC_USERNAME] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PASSWORD] + fpmdata["url"] = fields[revelation.entry.FIELD_GENERIC_HOSTNAME] + + elif data["type"] == revelation.entry.ENTRY_ACCOUNT_WEBSITE: + fpmdata["user"] = fields[revelation.entry.FIELD_GENERIC_USERNAME] + fpmdata["password"] = fields[revelation.entry.FIELD_GENERIC_PASSWORD] + fpmdata["url"] = fields[revelation.entry.FIELD_GENERIC_URL] + + return fpmdata + + + def __xml_export_entries(self, entrystore, parent = None): + xml = "" + + for i in range(entrystore.iter_n_children(parent)): + child = entrystore.iter_nth_child(parent, i) + data = entrystore.get_entry(child) + + if data["type"] == revelation.entry.ENTRY_FOLDER: + xml = xml + self.__xml_export_entries(entrystore, child) + + else: + + # convert data to fpm structure + fpmdata = self.__xml_export_convert_data(data) + + path = entrystore.get_path(child) + if len(path) > 1: + fpmdata["category"] = entrystore.get_entry(entrystore.get_iter(path[0]))["name"] + + xml = xml + "" + xml = xml + "" + self.__encrypt_field(fpmdata["title"]) + "" + xml = xml + "" + self.__encrypt_field(fpmdata["user"]) + "" + xml = xml + "" + self.__encrypt_field(fpmdata["url"]) + "" + xml = xml + "" + self.__encrypt_field(fpmdata["password"]) + "" + xml = xml + "" + self.__encrypt_field(fpmdata["notes"]) + "" + xml = xml + "" + self.__encrypt_field(fpmdata["category"]) + "" + xml = xml + "" + self.__encrypt_field(fpmdata["launcher"]) + "" + xml = xml + "" + + return xml + + + def __xml_import_entry(self, entry, entrystore, foldercache): + if entry.type != "element" or entry.name != "PasswordItem": + return + + parent = None + data = { + "type" : revelation.entry.ENTRY_ACCOUNT_GENERIC, + "updated" : time.time(), + "fields" : {} + } + + field = entry.children + while field is not None: + if field.type == "element": + content = self.__decrypt_field(field.content) + + if field.name == "title": + data["name"] = content + + elif field.name == "user": + data["fields"][revelation.entry.FIELD_GENERIC_USERNAME] = content + + elif field.name == "url": + data["fields"][revelation.entry.FIELD_GENERIC_HOSTNAME] = content + + elif field.name == "password": + data["fields"][revelation.entry.FIELD_GENERIC_PASSWORD] = content + + elif field.name == "notes": + data["description"] = content + + elif field.name == "category": + foldername = content.strip() + + if foldername == "": + pass + elif foldercache.has_key(foldername): + parent = foldercache[foldername] + else: + parent = entrystore.add_entry(None, {"type": revelation.entry.ENTRY_FOLDER, "name": foldername, "updated": time.time()}) + foldercache[foldername] = parent + + field = field.next + + entrystore.add_entry(parent, data) + + + def __xml_import_keyinfo(self, root): + keynode = self.xml_import_scan(root, "KeyInfo") + if keynode == None: + raise FormatError + + attrs = self.xml_import_attrs(keynode) + if not attrs.has_key("salt") or not attrs.has_key("vstring"): + raise FormatError + + info = { + "salt" : attrs["salt"], + "vstring" : attrs["vstring"] + } + + return info + + + def check_data(self, data): + if re.search("^<\?xml.*\?>\s*\n" + data = data + "" + data = data + "" + data = data + "" + data = data + "" + data = data + self.__xml_export_entries(entrystore) + data = data + "" + data = data + "" + + return data + + + def import_data(self, entrystore, data): + doc = self.xml_import_init(data) + + # fetch root node, and check validity + root = doc.children + if root.name != "FPM": + raise FormatError + + # set up cipher and check if password is correct + keyinfo = self.__xml_import_keyinfo(root) + self.cipher_init(Blowfish, MD5.new(keyinfo["salt"] + self.password).digest()) + + if self.__decrypt_field(keyinfo["vstring"]) != "FIGARO": + raise PasswordError + + # import entries into entrystore + pwlist = self.xml_import_scan(root, "PasswordList") + if pwlist == None: + raise FormatError + + foldercache = {} + entry = pwlist.children + while entry is not None: + self.__xml_import_entry(entry, entrystore, foldercache) + entry = entry.next + + + +class RevelationXMLData(Data): + + def __xml_import_node(self, entrystore, node, parent = None): + + if node.type == "text": + return + + if node.type != "element" or node.name != "entry": + raise FormatError + + data = { + "type" : self.xml_import_attrs(node)["type"], + "fields" : {} + } + + # convert obsolete types, for backwards-compatability + if data["type"] == "usenet": + data["type"] = revelation.entry.ENTRY_ACCOUNT_GENERIC + + if not revelation.entry.entry_exists(data["type"]): + raise EntryTypeError + + # add empty entry, iter needed for any children + iter = entrystore.add_entry(parent) + + child = node.children + while child is not None: + + if child.type == "element": + + if child.name == "name": + data["name"] = child.content + + elif child.name == "description": + data["description"] = child.content + + elif child.name == "updated": + data["updated"] = int(child.content) + + elif child.name == "field": + field = self.xml_import_attrs(child)["id"] + + if not revelation.entry.field_exists(data["type"], field): + raise EntryFieldError + + data["fields"][field] = child.content + + elif child.name == "entry": + self.__xml_import_node(entrystore, child, iter) + + else: + raise FormatError + + child = child.next + + # update entry with actual data + entrystore.update_entry(iter, data) + + + def check_data(self, data): + match = re.search("^\s*<\?xml.*\?>\s* revelation.DATAVERSION: + raise VersionError + + + def detect_type(self, data): + try: + self.check_data(data) + except FormatError: + return gtk.FALSE + else: + return gtk.TRUE + + + def export_data(self, entrystore, parent = None, level = 0): + xml = "" + tabs = "\t" * (level + 1) + + # process each child + for i in range(entrystore.iter_n_children(parent)): + iter = entrystore.iter_nth_child(parent, i) + data = entrystore.get_entry(iter) + + xml = xml + "\n" + xml = xml + tabs + "\n" + xml = xml + tabs + " " + revelation.misc.escape_markup(data["name"]) + "\n" + xml = xml + tabs + " " + revelation.misc.escape_markup(data["description"]) + "\n" + xml = xml + tabs + " " + revelation.misc.escape_markup(str(data["updated"])) + "\n" + + for field, value in data["fields"].items(): + xml = xml + tabs + " " + revelation.misc.escape_markup(value) + "\n" + + # handle any children + xml = xml + RevelationXMLData.export_data(self, entrystore, iter, level + 1) + + xml = xml + tabs + "\n" + + # generate header and footer if at level 0 + if level == 0: + header = "\n" + header = header + "\n" + footer = "\n" + xml = header + xml + footer + + return xml + + + def import_data(self, entrystore, data): + doc = self.xml_import_init(data) + + # fetch and validate root + root = doc.children + if root.name != "revelationdata": + raise FormatError + + attrs = self.xml_import_attrs(root) + if attrs.has_key("dataversion") and int(attrs["dataversion"]) > revelation.DATAVERSION: + raise VersionError + + # process entries + child = root.children + while child is not None: + self.__xml_import_node(entrystore, child) + child = child.next + + + +class RevelationData(RevelationXMLData): + + def __init__(self): + RevelationXMLData.__init__(self) + self.blocksize = 16 + self.keysize = 32 + self.password = None + + + def __decrypt(self, data, password, iv = None): + self.cipher_init(AES, password, iv, self.blocksize, self.keysize) + return self.cipher_decrypt(data) + + + def __encrypt(self, data, password, iv = None): + self.cipher_init(AES, password, iv, self.blocksize, self.keysize) + return self.cipher_encrypt(data) + + + def __generate_header(self): + header = "rvl\x00" + chr(revelation.DATAVERSION) + "\x00" + for part in revelation.VERSION.split("."): + header = header + chr(int(part)) + header = header + "\x00\x00\x00" + + return header + + + def __parse_header(self, header): + if len(header) != 12 or header[0:3] != "rvl": + return None + + dataversion = ord(header[4]) + appversion = str(ord(header[6])) + "." + str(ord(header[7])) + "." + str(ord(header[8])) + + return dataversion, appversion + + + def check_data(self, data): + headerdata = self.__parse_header(data[0:12]) + + # ignore version 0 data files (deprecated, remove in future version) + if headerdata == None: + return + + if headerdata[0] > revelation.DATAVERSION: + raise VersionError + + + def detect_type(self, header): + return self.__parse_header(header[0:12]) != None + + + def export_data(self, entrystore): + + # first, generate XML data from the entrystore + data = RevelationXMLData.export_data(self, entrystore) + + # next, compress the data and right-pad it + # (the pad is the repeated ascii value of the pad length) + data = zlib.compress(data) + + padlen = 16 - (len(data) % 16) + if padlen == 0: + padlen = 16 + + data = data + (chr(padlen) * padlen) + + # generate an initial vector for CBC encryption mode + iv = "" + for i in range(16): + iv = iv + chr(int(random.random() * 255)) + + # encrypt the data + data = self.__encrypt(data, self.password, iv) + + # encrypt the iv, and prepend it to the data along with a header + data = self.__generate_header() + self.__encrypt(iv, self.password) + data + + return data + + + def import_data(self, entrystore, data): + + # get the version numbers from the header + headerdata = self.__parse_header(data[0:12]) + + # if no valid header was found, assume version 0 (0.1.x series). + # this is deprecated, and support will be removed in a future version + if headerdata == None: + + data = self.__decrypt(data, self.password) + + # if not xml file, assume wrong password (could be invalid file + # as well, but in most cases it will be a wrong password) + if data[0:5] != "" + revelation.misc.escape_markup(pritext) + "\n\n" + sectext) + label.set_line_wrap(gtk.TRUE) + label.set_alignment(0, 0) + self.contents.pack_start(label) + + + def run(self): + self.show_all() + response = gtk.Dialog.run(self) + self.destroy() + + return response + + + +class Property(Dialog): + + def __init__(self, parent, title, buttons, default = None): + Dialog.__init__(self, parent, title, buttons, default) + + self.set_border_width(12) + self.vbox.set_spacing(18) + + self.sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + self.tooltips = gtk.Tooltips() + + + def add_section(self, title, description = None): + section = revelation.widget.InputSection(title, self.sizegroup, description) + self.vbox.pack_start(section) + return section + + + +# the following classes may be subclassed from the bases above +class About(gnome.ui.About): + + def __init__(self): + gnome.ui.About.__init__( + self, revelation.APPNAME, revelation.VERSION, revelation.COPYRIGHT, + "\"" + revelation.RELNAME + "\"\n\nRevelation is a password manager for the GNOME 2 desktop.", + [ revelation.AUTHOR ], None, "", + gtk.gdk.pixbuf_new_from_file(revelation.DATADIR + "/pixmaps/revelation.png") + ) + + + def run(self): + self.show_all() + + + +class EditEntry(Property): + + def __init__(self, parent, title, data = None): + Property.__init__( + self, parent, title, + [ [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ gtk.STOCK_OK, gtk.RESPONSE_OK ] ] + ) + + if data is not None: + self.data = data.copy() + self.data["fields"] = self.data["fields"].copy() + else: + self.data = revelation.entry.get_entry_template(revelation.entry.ENTRY_FOLDER) + + section = self.add_section(title) + + entry = revelation.widget.Entry(self.data["name"]) + entry.set_width_chars(50) + entry.connect("changed", self.__cb_entry_changed, "name") + self.tooltips.set_tip(entry, "The name of the entry") + section.add_inputrow("Name", entry) + + entry = revelation.widget.Entry(self.data["description"]) + self.tooltips.set_tip(entry, "A description of the entry") + entry.connect("changed", self.__cb_entry_changed, "description") + section.add_inputrow("Description", entry) + + self.dropdown = revelation.widget.EntryDropdown() + self.tooltips.set_tip(self.dropdown, "The type of entry - folders can contain other entries") + section.add_inputrow("Type", self.dropdown) + + self.dropdown.set_type(self.data["type"]) + self.dropdown.connect("changed", self.__cb_dropdown_changed) + + self.update() + + + def __cb_entry_changed(self, widget, name): + self.data[name] = widget.get_text() + + def __cb_entry_field_changed(self, widget, name): + self.data["fields"][name] = widget.get_text() + + def __cb_dropdown_changed(self, object): + type = self.dropdown.get_active_item().type + + if type != self.data["type"]: + self.data["type"] = type + self.data["icon"] = revelation.entry.get_entry_data(type, "icon") + self.update() + + + def run(self): + if Property.run(self) == gtk.RESPONSE_OK: + + if self.data["name"] == "": + Error(self, "No name given", "You need to enter a name for the entry.").run() + return self.run() + + # normalize data + self.data["updated"] = int(time.time()) + + for field in self.data["fields"].keys(): + if not revelation.entry.field_exists(self.data["type"], field): + del self.data["fields"][field] + + self.destroy() + return self.data + + else: + self.destroy() + raise revelation.CancelError + + + def set_typechange_allowed(self, allow): + self.dropdown.set_sensitive(allow) + + + def update(self, type = None): + if len(self.vbox.get_children()) > 2: + self.vbox.get_children().pop(1).destroy() + + fields = revelation.entry.get_entry_fields(self.data["type"]) + + if len(fields) > 0: + section = self.add_section("Account data") + + for field in fields: + fielddata = revelation.entry.get_field_data(field) + + if field == revelation.entry.FIELD_GENERIC_PASSWORD: + entry = revelation.widget.PasswordEntry() + + elif fielddata["type"] == revelation.entry.FIELD_TYPE_PASSWORD: + entry = revelation.widget.PasswordEntry(None, gtk.FALSE) + + else: + entry = revelation.widget.Entry() + + entry.set_text(self.data["fields"].get(field, "")) + entry.connect("changed", self.__cb_entry_field_changed, field) + self.tooltips.set_tip(entry, fielddata["tooltip"]) + section.add_inputrow(fielddata["name"], entry) + + self.show_all() + + + +class Error(Hig): + + def __init__(self, parent, pritext, sectext): + Hig.__init__( + self, parent, pritext, sectext, gtk.STOCK_DIALOG_ERROR, + [ [ gtk.STOCK_OK, gtk.RESPONSE_OK ] ] + ) + + + +class FileOverwrite(Hig): + + def __init__(self, parent, file): + Hig.__init__( + self, parent, "Overwrite existing file?", + "The file '" + file + "' already exists. If you choose to overwrite the file, its contents will be lost.", gtk.STOCK_DIALOG_WARNING, + [ [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ revelation.stock.STOCK_OVERWRITE, gtk.RESPONSE_OK ] ], + gtk.RESPONSE_CANCEL + ) + + + def run(self): + return Hig.run(self) == gtk.RESPONSE_OK + + + +class FileSelector(gtk.FileSelection): + + def __init__(self, title = None): + gtk.FileSelection.__init__(self, title) + + + def run(self): + self.show() + response = gtk.FileSelection.run(self) + filename = self.get_filename() + self.destroy() + + if response == gtk.RESPONSE_OK: + return filename + else: + raise revelation.CancelError + + + +class Find(Dialog): + + def __init__(self, parent): + Dialog.__init__( + self, parent, "Find an entry", + [ [ gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE ], [ revelation.stock.STOCK_PREVIOUS, RESPONSE_PREVIOUS ], [ revelation.stock.STOCK_NEXT, RESPONSE_NEXT ] ] + ) + + self.connect("key-press-event", self.__cb_keypress) + self.tooltips = gtk.Tooltips() + + section = revelation.widget.InputSection("Find an entry") + self.vbox.pack_start(section) + + self.entry_phrase = revelation.widget.Entry() + self.tooltips.set_tip(self.entry_phrase, "The text to search for") + self.entry_phrase.connect("changed", self.__cb_entry_changed) + section.add_inputrow("Search for", self.entry_phrase) + + self.dropdown = revelation.widget.EntryDropdown() + self.tooltips.set_tip(self.dropdown, "The account type to search for") + item = self.dropdown.get_item(0) + item.set_stock("gnome-stock-about") + item.set_text("Any") + item.type = None + section.add_inputrow("Account type", self.dropdown) + + check = revelation.widget.CheckButton("Search for folders as well") + self.tooltips.set_tip(check, "When enabled, folder names and descriptions will also be searched") + check.gconf_bind("/apps/revelation/search/folders") + section.add_inputrow(None, check) + + check = revelation.widget.CheckButton("Only search in name and description") + self.tooltips.set_tip(check, "When enabled, only entry names and descriptions will be searched") + check.gconf_bind("/apps/revelation/search/namedesc") + section.add_inputrow(None, check) + + check = revelation.widget.CheckButton("Case sensitive") + self.tooltips.set_tip(check, "When enabled, searches will be case sensitive") + check.gconf_bind("/apps/revelation/search/casesens") + section.add_inputrow(None, check) + + # set up initial states + self.entry_phrase.emit("changed") + + + def __cb_entry_changed(self, widget, data = None): + active = len(self.entry_phrase.get_text()) > 0 + self.get_button(0).set_sensitive(active) + self.get_button(1).set_sensitive(active) + + + def __cb_keypress(self, widget, data): + if data.keyval == 65307: + self.response(gtk.RESPONSE_CLOSE) + + + def run(self): + self.show_all() + return Dialog.run(self) + + + +class Password(Hig): + + def __init__(self, parent, title, text, pwlen = 32, current = gtk.TRUE, new = gtk.FALSE): + Hig.__init__( + self, parent, title, text, revelation.stock.STOCK_PASSWORD, + [ [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ gtk.STOCK_OK, gtk.RESPONSE_OK ] ] + ) + + self.entry_password = None + self.entry_new = None + self.entry_confirm = None + + section = revelation.widget.InputSection() + self.contents.pack_start(section) + + if current == gtk.TRUE or new == gtk.FALSE: + self.entry_password = revelation.widget.Entry() + self.entry_password.set_visibility(gtk.FALSE) + self.entry_password.set_max_length(pwlen) + section.add_inputrow("Password", self.entry_password) + + if new == gtk.TRUE: + self.entry_new = revelation.widget.Entry() + self.entry_new.set_visibility(gtk.FALSE) + self.entry_new.set_max_length(pwlen) + section.add_inputrow("New password", self.entry_new) + + self.entry_confirm = revelation.widget.Entry() + self.entry_confirm.set_visibility(gtk.FALSE) + self.entry_confirm.set_max_length(pwlen) + section.add_inputrow("Confirm new", self.entry_confirm) + + + def run(self): + while 1: + self.show_all() + + if self.entry_password != None: + self.entry_password.grab_focus() + elif self.entry_new != None: + self.entry_new.grab_focus() + + if Dialog.run(self) == gtk.RESPONSE_OK: + + if self.entry_new != None and self.entry_new.get_text() == "": + Error(self, "New password not given", "You need to enter a new password.").run() + + elif self.entry_new != None and self.entry_new.get_text() != self.entry_confirm.get_text(): + Error(self, "Passwords don't match", "The password and password confirmation you entered does not match.").run() + + else: + break + + else: + raise revelation.CancelError + + + +class Preferences(Property): + + def __init__(self, parent): + Property.__init__(self, parent, "Preferences", [ [ gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE ] ]) + + self.__init_section_file() + self.__init_section_pwgen() + + + def __init_section_file(self): + self.section_file = self.add_section("File Handling") + + self.check_autoload = revelation.widget.CheckButton("Open file on startup") + self.check_autoload.gconf_bind("/apps/revelation/file/autoload") + self.check_autoload.connect("toggled", self.__cb_file_autoload) + self.tooltips.set_tip(self.check_autoload, "When enabled, a file will be opened when the program is started") + self.section_file.add_inputrow(None, self.check_autoload) + + self.entry_autoload_file = revelation.widget.FileEntry("revelation-autoload", "Select File to Automatically Open") + self.entry_autoload_file.gconf_bind("/apps/revelation/file/autoload_file") + self.entry_autoload_file.set_sensitive(self.check_autoload.get_active()) + self.tooltips.set_tip(self.entry_autoload_file, "A file to be opened when the program is started") + self.section_file.add_inputrow("File to open", self.entry_autoload_file) + + + def __init_section_pwgen(self): + self.section_pwgen = self.add_section("Password Generator") + + self.spin_pwlen = revelation.widget.SpinButton() + self.spin_pwlen.set_range(4, 32) + self.spin_pwlen.gconf_bind("/apps/revelation/passwordgen/length") + self.tooltips.set_tip(self.spin_pwlen, "The number of characters in generated passwords - 8 or more are recommended") + self.section_pwgen.add_inputrow("Password length", self.spin_pwlen) + + self.check_ambiguous = revelation.widget.CheckButton("Avoid ambiguous characters") + self.check_ambiguous.gconf_bind("/apps/revelation/passwordgen/avoid_ambiguous") + self.tooltips.set_tip(self.check_ambiguous, "When enabled, generated passwords will not contain ambiguous characters - like 0 (zero) and O (capital o)") + self.section_pwgen.add_inputrow(None, self.check_ambiguous) + + + def __cb_file_autoload(self, object, data = None): + self.entry_autoload_file.set_sensitive(self.check_autoload.get_active()) + + + def run(self): + self.show_all() + Property.run(self) + self.destroy() + + + +class RemoveEntry(Hig): + + def __init__(self, parent, pritext, sectext): + Hig.__init__( + self, parent, pritext, sectext, gtk.STOCK_DIALOG_WARNING, + [ [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ revelation.stock.STOCK_REMOVE, gtk.RESPONSE_OK ] ], + gtk.RESPONSE_CANCEL + ) + + + def run(self): + return Hig.run(self) == gtk.RESPONSE_OK + + +class SaveChanges(Hig): + + def __init__(self, parent, pritext, sectext): + Hig.__init__( + self, parent, pritext, sectext, gtk.STOCK_DIALOG_WARNING, + [ [ revelation.stock.STOCK_DISCARD, gtk.RESPONSE_CLOSE ], [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ gtk.STOCK_SAVE, gtk.RESPONSE_OK ] ] + ) + + def run(self): + response = Hig.run(self) + + if response == gtk.RESPONSE_CANCEL: + raise revelation.CancelError + else: + return response == gtk.RESPONSE_OK + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/druid.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/druid.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,360 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module containing druid classes +# +# +# Copyright (c) 2003-2004 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 gtk, gnome.ui, revelation, os + +class Page(gnome.ui.DruidPageStandard): + + def __init__(self, title, logo = None): + gnome.ui.DruidPageStandard.__init__(self) + self.set_title(title) + + if logo == None: + logo = self.render_icon(revelation.stock.STOCK_APPLICATION, revelation.stock.ICON_SIZE_DRUID) + + self.set_logo(logo) + + + def append_item(self, text, widget, extratext = None): + vbox = gtk.VBox() + vbox.set_spacing(18) + vbox.set_border_width(6) + gnome.ui.DruidPageStandard.append_item(self, "", vbox, "") + + if text != None: + vbox.pack_start(revelation.widget.Label(text, gtk.JUSTIFY_CENTER)) + + vbox.pack_start(widget, gtk.FALSE, gtk.FALSE) + + if extratext != None: + vbox.pack_start(revelation.widget.Label("" + extratext + "", gtk.JUSTIFY_CENTER)) + + + +class PageEdge(gnome.ui.DruidPageEdge): + + def __init__(self, position, title, text = None, logo = None): + gnome.ui.DruidPageEdge.__init__(self, position) + self.set_title(title) + self.pagepos = position + + if text != None: + self.set_text(text) + + if logo == None: + logo = self.render_icon(revelation.stock.STOCK_APPLICATION, revelation.stock.ICON_SIZE_DRUID, None) + + self.set_logo(logo) + + + +class Druid(gnome.ui.Druid): + + def __init__(self, parent, title): + gnome.ui.Druid.__init__(self) + self.connect("cancel", self.__cb_cancel) + + self.dialog = gtk.Dialog(title, parent, (gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_MODAL | gtk.DIALOG_NO_SEPARATOR)) + self.dialog.action_area.destroy() + self.dialog.vbox.pack_start(self) + + + def __cb_cancel(self, object): + self.dialog.response(gtk.RESPONSE_CANCEL) + + def __cb_finish(self, object, data): + self.dialog.response(gtk.RESPONSE_OK) + + def __cb_destroy_page(self, page, data): + page.destroy() + + + def append_page(self, page): + if hasattr(page, "pagepos") and page.pagepos == gnome.ui.EDGE_FINISH: + page.connect("finish", self.__cb_finish) + gnome.ui.Druid.append_page(self, page) + + + def insert_page(self, prevpage, page): + gnome.ui.Druid.insert_page(self, prevpage, page) + page.connect("back", self.__cb_destroy_page) + page.show_all() + + + def run(self): + self.dialog.show_all() + response = self.dialog.run() + self.destroy() + self.dialog.destroy() + + return response + + + +class ExportFile(Druid): + + def __init__(self, parent): + Druid.__init__(self, parent, "Export File") + self.filetypes = revelation.datafile.FileTypes() + self.datafile = revelation.datafile.DataFile() + + self.append_page(self.__page_start()) + self.append_page(self.__page_file()) + self.append_page(self.__page_finish()) + + + def __page_file(self): + page = Page("Select File to Export to") + page.connect("next", self.__cb_page_file) + + section = revelation.widget.InputSection() + page.append_item("Select the file you wish to export data to, and the file type:", section) + + page.entry_file = revelation.widget.FileEntry("revelation-export", "Select File to Export to") + section.add_inputrow("File", page.entry_file) + + page.dropdown = revelation.widget.OptionMenu() + section.add_inputrow("Filetype", page.dropdown) + + for type in self.filetypes.get_export_types(): + item = gtk.MenuItem(self.filetypes.get_data(type, "name")) + item.filetype = type + page.dropdown.append_item(item) + + page.dropdown.connect("changed", self.__cb_filetype_changed, page.entry_file) + + return page + + + def __page_start(self): + return PageEdge(gnome.ui.EDGE_START, "File Export Assistant", "Welcome to the Revelation file export assistant, which will guide you through exporting accounts into a foreign file format.") + + + def __page_finish(self): + return PageEdge(gnome.ui.EDGE_FINISH, "Export File", "You have now provided all information needed to export the data. Press \"Apply\" to start the export.") + + + def __page_password(self): + page = Page("Enter Password") + page.connect("next", self.__cb_page_password) + + section = revelation.widget.InputSection() + page.append_item("Please enter a password to encrypt the file with:", section) + + page.entry_password = revelation.widget.Entry() + page.entry_confirm = revelation.widget.Entry() + + for entry, name in zip((page.entry_password, page.entry_confirm), ("Password", "Confirm password")): + entry.set_visibility(gtk.FALSE) + entry.set_text(self.datafile.password) + section.add_inputrow(name, entry) + + return page + + + def __cb_filetype_changed(self, dropdown, entry): + type = dropdown.get_active_item().filetype + default = self.filetypes.get_data(type, "defaultfile") + + if default != None and entry.gtk_entry().get_text() == "": + entry.set_filename(default) + + + def __cb_page_file(self, page, data): + self.datafile.type = page.dropdown.get_active_item().filetype + file = page.entry_file.get_full_path(gtk.FALSE) + + if file == None: + revelation.dialog.Error(self.dialog, "Filename not entered", "You need to enter a file to export the data to.").run() + return gtk.TRUE + + if os.access(file, os.F_OK) == 1 and revelation.dialog.FileOverwrite(self.dialog, file).run() == gtk.FALSE: + return gtk.TRUE + + self.datafile.file = file + + # check if file format is insecure + if not hasattr(self.datafile, "password"): + if revelation.dialog.Hig( + self.dialog, "Export to insecure file?", "The file format you have chosen is not encrypted. If anyone has access to the file, they will be able to read your passwords.", + gtk.STOCK_DIALOG_WARNING, [ [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ revelation.stock.STOCK_EXPORT, gtk.RESPONSE_OK ] ] + ).run() == gtk.RESPONSE_CANCEL: + return gtk.TRUE + + else: + # set up password page if a password is needed + self.insert_page(page, self.__page_password()) + + + def __cb_page_password(self, page, data): + + # fetch passwords + password = page.entry_password.get_text() + confirm = page.entry_confirm.get_text() + + if len(password) == 0: + revelation.dialog.Error(self.dialog, "No password given", "You must enter a password to encrypt the file with.").run() + return gtk.TRUE + + if password != confirm: + revelation.dialog.Error(self.dialog, "Passwords don't match", "The passwords you entered do not match each other. Make sure that you enter the same password in both input fields.").run() + return gtk.TRUE + + self.datafile.password = password + + + def run(self): + if Druid.run(self) == gtk.RESPONSE_OK: + return self.datafile + else: + raise revelation.CancelError + + + +class ImportFile(Druid): + + def __init__(self, parent): + Druid.__init__(self, parent, "Import File") + self.filetypes = revelation.datafile.FileTypes() + self.datafile = revelation.datafile.DataFile() + + self.append_page(self.__page_start()) + self.append_page(self.__page_file()) + self.append_page(self.__page_finish()) + + + def __page_file(self): + page = Page("Select File to Import") + page.connect("next", self.__cb_page_file) + + section = revelation.widget.InputSection() + page.append_item("Select the file you wish to import, and the type of file:", section, "You can select \"Autodetect\" to make Revelation attempt to find the filetype itself.") + + page.entry_file = revelation.widget.FileEntry("revelation-import", "Select File to Import") + section.add_inputrow("File", page.entry_file) + + page.dropdown = revelation.widget.OptionMenu() + section.add_inputrow("Filetype", page.dropdown) + + item = gtk.MenuItem("Autodetect") + item.filetype = revelation.datafile.TYPE_AUTO + page.dropdown.append_item(item) + + page.dropdown.append_item(gtk.SeparatorMenuItem()) + + for type in self.filetypes.get_import_types(): + item = gtk.MenuItem(self.filetypes.get_data(type, "name")) + item.filetype = type + page.dropdown.append_item(item) + + page.dropdown.connect("changed", self.__cb_filetype_changed, page.entry_file) + + return page + + + def __page_finish(self): + return PageEdge(gnome.ui.EDGE_FINISH, "Import File", "You have now provided all required information to import the data. Press \"Apply\" to start the import.") + + + def __page_password(self): + page = Page("Enter Password") + page.connect("next", self.__cb_page_password) + + section = revelation.widget.InputSection() + page.append_item("This file is encrypted - please enter a password to decrypt it:", section) + + page.entry_password = revelation.widget.Entry() + page.entry_password.set_visibility(gtk.FALSE) + section.add_inputrow("Password", page.entry_password) + + if self.datafile.password != None: + page.entry_password.set_text(self.datafile.password) + + return page + + + def __page_start(self): + return PageEdge(gnome.ui.EDGE_START, "File Import Assistant", "Welcome to the Revelation file import assistant, which will guide you through importing accounts from other password managers.") + + + # callbacks + def __cb_filetype_changed(self, dropdown, entry): + type = dropdown.get_active_item().filetype + default = self.filetypes.get_data(type, "defaultfile") + + if default != None and entry.gtk_entry().get_text() == "": + entry.set_filename(default) + + + def __cb_page_file(self, page, widget): + try: + self.datafile.file = page.entry_file.get_filename() + self.datafile.check_file() + + type = page.dropdown.get_active_item().filetype + + if type == revelation.datafile.TYPE_AUTO: + self.datafile.detect_type() + else: + self.datafile.type = type + + self.datafile.check_format() + + except IOError: + revelation.dialog.Error(self.dialog, "Unable to open file", "The file you selected could not be opened. Please make sure the file exists, and that you have the proper permissions to open it.").run() + return gtk.TRUE + + except revelation.datafile.FormatError: + revelation.dialog.Error(self.dialog, "Invalid file format", "The file '" + self.datafile.file + "' is not a valid " + self.filetypes.get_data(self.datafile.type, "name") + " file.").run() + return gtk.TRUE + + except revelation.datafile.VersionError: + revelation.dialog.Error(self.dialog, "Future file version", "The file '" + self.datafile.file + "' is from an unknown version. Please upgrade Revelation to a newer version to import it.").run() + return gtk.TRUE + + except revelation.datafile.DetectError: + revelation.dialog.Error(self.dialog, "Autodetection failed", "Revelation was not able to autodetect the format of the file '" + self.datafile.file + "'. It may still be able to load the file, try specifying the file type manually.").run() + return gtk.TRUE + + # set up password page if needed + if hasattr(self.datafile, "password"): + self.insert_page(page, self.__page_password()) + + + def __cb_page_password(self, page, widget): + password = page.entry_password.get_text() + + if len(password) > 0: + self.datafile.password = password + else: + revelation.dialog.Error(self.dialog, "No password given", "You must enter a password to decrypt the file.").run() + return gtk.TRUE + + + def run(self): + if Druid.run(self) == gtk.RESPONSE_OK: + return self.datafile + else: + raise revelation.CancelError + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/entry.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/entry.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,289 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module containing entry information +# +# +# Copyright (c) 2003-2004 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 revelation + + +ENTRY_FOLDER = "folder" +ENTRY_ACCOUNT_CREDITCARD = "creditcard" +ENTRY_ACCOUNT_CRYPTOKEY = "cryptokey" +ENTRY_ACCOUNT_DATABASE = "database" +ENTRY_ACCOUNT_DOOR = "door" +ENTRY_ACCOUNT_EMAIL = "email" +ENTRY_ACCOUNT_FTP = "ftp" +ENTRY_ACCOUNT_GENERIC = "generic" +ENTRY_ACCOUNT_PHONE = "phone" +ENTRY_ACCOUNT_SHELL = "shell" +ENTRY_ACCOUNT_WEBSITE = "website" + + +FIELD_GENERIC_CERTIFICATE = "generic-certificate" +FIELD_GENERIC_CODE = "generic-code" +FIELD_GENERIC_DATABASE = "generic-database" +FIELD_GENERIC_DOMAIN = "generic-domain" +FIELD_GENERIC_EMAIL = "generic-email" +FIELD_GENERIC_HOSTNAME = "generic-hostname" +FIELD_GENERIC_KEYFILE = "generic-keyfile" +FIELD_GENERIC_LOCATION = "generic-location" +FIELD_GENERIC_PASSWORD = "generic-password" +FIELD_GENERIC_PIN = "generic-pin" +FIELD_GENERIC_PORT = "generic-port" +FIELD_GENERIC_URL = "generic-url" +FIELD_GENERIC_USERNAME = "generic-username" + +FIELD_CREDITCARD_CARDTYPE = "creditcard-cardtype" +FIELD_CREDITCARD_CARDNUMBER = "creditcard-cardnumber" +FIELD_CREDITCARD_CCV = "creditcard-ccv" +FIELD_CREDITCARD_EXPIRYDATE = "creditcard-expirydate" + +FIELD_PHONE_PHONENUMBER = "phone-phonenumber" + + +FIELD_TYPE_EMAIL = "email" +FIELD_TYPE_PASSWORD = "password" +FIELD_TYPE_TEXT = "text" +FIELD_TYPE_URL = "url" + + +ENTRYDATA = { + ENTRY_FOLDER : { + "name" : "Folder", + "icon" : revelation.stock.STOCK_FOLDER, + "fields" : [] + }, + + ENTRY_ACCOUNT_CREDITCARD : { + "name" : "Creditcard", + "icon" : revelation.stock.STOCK_ACCOUNT_CREDITCARD, + "fields" : [ FIELD_CREDITCARD_CARDTYPE, FIELD_CREDITCARD_CARDNUMBER, FIELD_CREDITCARD_EXPIRYDATE, FIELD_CREDITCARD_CCV, FIELD_GENERIC_PIN ] + }, + + ENTRY_ACCOUNT_CRYPTOKEY : { + "name" : "Crypto Key", + "icon" : revelation.stock.STOCK_ACCOUNT_CRYPTOKEY, + "fields" : [ FIELD_GENERIC_HOSTNAME, FIELD_GENERIC_CERTIFICATE, FIELD_GENERIC_KEYFILE, FIELD_GENERIC_PASSWORD ] + }, + + ENTRY_ACCOUNT_DATABASE : { + "name" : "Database", + "icon" : revelation.stock.STOCK_ACCOUNT_DATABASE, + "fields" : [ FIELD_GENERIC_HOSTNAME, FIELD_GENERIC_USERNAME, FIELD_GENERIC_PASSWORD, FIELD_GENERIC_DATABASE ] + }, + + ENTRY_ACCOUNT_DOOR : { + "name" : "Door lock", + "icon" : revelation.stock.STOCK_ACCOUNT_DOOR, + "fields" : [ FIELD_GENERIC_LOCATION, FIELD_GENERIC_CODE ] + }, + + ENTRY_ACCOUNT_EMAIL : { + "name" : "Email", + "icon" : revelation.stock.STOCK_ACCOUNT_EMAIL, + "fields" : [ FIELD_GENERIC_EMAIL, FIELD_GENERIC_HOSTNAME, FIELD_GENERIC_USERNAME, FIELD_GENERIC_PASSWORD ] + }, + + ENTRY_ACCOUNT_FTP : { + "name" : "FTP", + "icon" : revelation.stock.STOCK_ACCOUNT_FTP, + "fields" : [ FIELD_GENERIC_HOSTNAME, FIELD_GENERIC_PORT, FIELD_GENERIC_USERNAME, FIELD_GENERIC_PASSWORD ] + }, + + ENTRY_ACCOUNT_GENERIC : { + "name" : "Generic", + "icon" : revelation.stock.STOCK_ACCOUNT, + "fields" : [ FIELD_GENERIC_HOSTNAME, FIELD_GENERIC_USERNAME, FIELD_GENERIC_PASSWORD ] + }, + + ENTRY_ACCOUNT_PHONE : { + "name" : "Phone", + "icon" : revelation.stock.STOCK_ACCOUNT_PHONE, + "fields" : [ FIELD_PHONE_PHONENUMBER, FIELD_GENERIC_PIN ] + }, + + ENTRY_ACCOUNT_SHELL : { + "name" : "Shell", + "icon" : revelation.stock.STOCK_ACCOUNT_SHELL, + "fields" : [ FIELD_GENERIC_HOSTNAME, FIELD_GENERIC_DOMAIN, FIELD_GENERIC_USERNAME, FIELD_GENERIC_PASSWORD ] + }, + + ENTRY_ACCOUNT_WEBSITE : { + "name" : "Website", + "icon" : revelation.stock.STOCK_ACCOUNT_WEBSITE, + "fields" : [ FIELD_GENERIC_URL, FIELD_GENERIC_USERNAME, FIELD_GENERIC_PASSWORD ] + } +} + + +FIELDDATA = { + FIELD_GENERIC_CODE : { + "name" : "Code", + "type" : FIELD_TYPE_PASSWORD, + "tooltip" : "A code used to provide access to something" + }, + + FIELD_GENERIC_CERTIFICATE : { + "name" : "Certificate", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A certificate, such as an X.509 SSL Certificate" + }, + + FIELD_GENERIC_DATABASE : { + "name" : "Database", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A database name" + }, + + FIELD_GENERIC_DOMAIN : { + "name" : "Domain", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "An Internet or logon domain, like amazon.com or a Windows logon domain" + }, + + FIELD_GENERIC_EMAIL : { + "name" : "Email address", + "type" : FIELD_TYPE_EMAIL, + "tooltip" : "An email address" + }, + + FIELD_GENERIC_HOSTNAME : { + "name" : "Hostname", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "The name of a computer, like computer.domain.com or MYCOMPUTER" + }, + + FIELD_GENERIC_KEYFILE : { + "name" : "Key File", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A key file, used for authentication for example via ssh or to encrypt X.509 certificates" + }, + + FIELD_GENERIC_LOCATION : { + "name" : "Location", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A physical location, like office entrance" + }, + + FIELD_GENERIC_PASSWORD : { + "name" : "Password", + "type" : FIELD_TYPE_PASSWORD, + "tooltip" : "A secret word or character combination used for proving you have access" + }, + + FIELD_GENERIC_PIN : { + "name" : "PIN", + "type" : FIELD_TYPE_PASSWORD, + "tooltip" : "A Personal Identification Number, a numeric code used for credit cards, phones etc" + }, + + FIELD_GENERIC_PORT : { + "name" : "Port number", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A network port number, used to access network services directly" + }, + + FIELD_GENERIC_URL : { + "name" : "URL", + "type" : FIELD_TYPE_URL, + "tooltip" : "A Uniform Resource Locator, such as a web-site address" + }, + + FIELD_GENERIC_USERNAME : { + "name" : "Username", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A name or other identification used to identify yourself" + }, + + FIELD_CREDITCARD_CARDTYPE : { + "name" : "Card type", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "The type of creditcard, like MasterCard or VISA" + }, + + FIELD_CREDITCARD_CARDNUMBER : { + "name" : "Card number", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "The number of a creditcard, usually a 16-digit number" + }, + + FIELD_CREDITCARD_CCV : { + "name" : "CCV number", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A Credit Card Verification number, normally a 3-digit code found on the back of a card" + }, + + FIELD_CREDITCARD_EXPIRYDATE : { + "name" : "Expiry date", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "The month that the credit card validity expires" + }, + + FIELD_PHONE_PHONENUMBER : { + "name" : "Phone number", + "type" : FIELD_TYPE_TEXT, + "tooltip" : "A telephone number" + } +} + + + +def field_exists(type, field): + return field in ENTRYDATA[type]["fields"] + +def get_field_data(field, attr = None): + return attr is None and FIELDDATA[field] or FIELDDATA[field][attr] + +def get_field_type(field): + return FIELDDATA[field]["type"] + + + +def entry_exists(entry): + return ENTRYDATA.has_key(entry) + +def get_entry_fields(entry): + return ENTRYDATA[entry]["fields"] + +def get_entry_list(): + list = ENTRYDATA.keys() + list.sort() + return list + +def get_entry_template(entry): + data = { + "name" : "", + "description" : "", + "type" : entry, + "icon" : get_entry_data(entry, "icon"), + "updated" : 0, + "fields" : {} + } + + for field in get_entry_fields(entry): + data["fields"][entry] = "" + + return data + +def get_entry_data(type, attr = None): + return attr is None and ENTRYDATA[type] or ENTRYDATA[type][attr] + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/misc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/misc.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,145 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module with miscellaneous functionality +# +# +# Copyright (c) 2003-2004 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 time, random, gconf, gtk + + +def escape_markup(string): + string = string.replace("&", "&") + string = string.replace("<", "<") + string = string.replace(">", ">") + return string + + + +def generate_password(): + + def get_random_item(list): + return list[int(random.random() * len(list))] + + + client = gconf.client_get_default() + pwlen = client.get_int("/apps/revelation/passwordgen/length") + + # set up list of chars to exclude + exclude = [] + if client.get_bool("/apps/revelation/passwordgen/avoid_ambiguous") == gtk.TRUE: + exclude = [ "0", "O", "I", "l", "1", "S", "5", "q", "g" ] + + + # calculate the minimum share of the password for each character class + sharedigits = int(round(pwlen * 0.15)) + sharechars = int(round(pwlen * 0.24)) + sharecaps = int(round(pwlen * 0.24)) + + # set up a set of all usable ascii characters + digits = range(48, 58) + chars = range(65, 91) + caps = range(97, 123) + + full = [] + full.extend(digits) + full.extend(chars) + full.extend(caps) + + + # generate characters for the password + pwchars = [] + while len(pwchars) < pwlen: + + # get digits + if len(pwchars) < sharedigits: + char = chr(get_random_item(digits)) + + # get chars + elif len(pwchars) < sharechars + sharedigits: + char = chr(get_random_item(chars)) + + # get caps + elif len(pwchars) < sharecaps + sharechars + sharedigits: + char = chr(get_random_item(caps)) + + # get random characters + else: + char = chr(get_random_item(full)) + + + if char not in exclude: + pwchars.append(char) + + + # shuffle the password + password = "" + for i in range(len(pwchars)): + password += pwchars.pop(int(random.random() * len(pwchars))) + + return password + + + +def timediff_simple(start, end = None): + if end is None: + end = time.time() + + if end < start: + return None + + diff = int(end - start) + + gmstart = time.gmtime(start) + gmend = time.gmtime(end) + + if diff >= int(365.25 * 24 * 60 * 60): + period = int(diff / 365.25 / 24 / 60 / 60) + unit = "year" + + elif gmend.tm_mon - gmstart.tm_mon >= 2 or (gmend.tm_mon - gmstart.tm_mon == 1 and gmend.tm_mday >= gmstart.tm_mday): + period = gmend.tm_mon - gmstart.tm_mon + unit = "month" + + elif diff >= 7 * 24 * 60 * 60: + period = diff / 7 / 24 / 60 / 60 + unit = "week" + + elif diff >= 24 * 60 * 60: + period = diff / 24 / 60 / 60 + unit = "day" + + elif diff >= 60 * 60: + period = diff / 60 / 60 + unit = "hour" + + elif diff >= 60: + period = diff / 60 + unit = "minute" + + else: + period = diff + unit = "second" + + if period == 1: + return str(period) + " " + unit + else: + return str(period) + " " + unit + "s" + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/stock.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/stock.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,125 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module containing stock items and related functionality +# +# +# Copyright (c) 2003-2004 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 gtk, gtk.gdk, revelation + + +STOCK_ADD = "revelation-add" +STOCK_DISCARD = "revelation-discard" +STOCK_EDIT = "revelation-edit" +STOCK_EXPORT = "revelation-export" +STOCK_IMPORT = "revelation-import" +STOCK_LOCK = "revelation-lock" +STOCK_NEXT = "revelation-next" +STOCK_OVERWRITE = "revelation-overwrite" +STOCK_PREVIOUS = "revelation-previous" +STOCK_REMOVE = "revelation-remove" + +STOCK_ACCOUNT_CREDITCARD = "revelation-account-creditcard" +STOCK_ACCOUNT_CRYPTOKEY = "revelation-account-cryptokey" +STOCK_ACCOUNT_DATABASE = "revelation-account-database" +STOCK_ACCOUNT_DOOR = "revelation-account-door" +STOCK_ACCOUNT_EMAIL = "revelation-account-email" +STOCK_ACCOUNT_FTP = "revelation-account-ftp" +STOCK_ACCOUNT_GENERIC = "revelation-account-generic" +STOCK_ACCOUNT_PHONE = "revelation-account-phone" +STOCK_ACCOUNT_SHELL = "revelation-account-shell" +STOCK_ACCOUNT_WEBSITE = "revelation-account-website" + +STOCK_ACCOUNT = STOCK_ACCOUNT_GENERIC +STOCK_APPLICATION = "revelation-application" +STOCK_FOLDER = "revelation-folder" +STOCK_FOLDER_OPEN = "revelation-folder-open" +STOCK_PASSWORD = "revelation-password" + + +ICON_SIZE_DATAVIEW = gtk.icon_size_register("revelation-dataview", 24, 24) +ICON_SIZE_DROPDOWN = gtk.icon_size_register("revelation-dropdown", 16, 16) +ICON_SIZE_DRUID = gtk.icon_size_register("revelation-druid", 48, 48) + +# this one needs to be a normal gtk icon size, as the tree cellrenderer +# seems to have problems with custom ones. +ICON_SIZE_TREEVIEW = gtk.ICON_SIZE_SMALL_TOOLBAR + + +gtk.stock_add(( + (STOCK_ADD, "_Add Entry", 0, 0, None), + (STOCK_DISCARD, "_Discard", 0, 0, None), + (STOCK_EDIT, "_Edit", 0, 0, None), + (STOCK_EXPORT, "_Export", 0, 0, None), + (STOCK_IMPORT, "_Import", 0, 0, None), + (STOCK_LOCK, "_Lock", 0, 0, None), + (STOCK_NEXT, "Ne_xt", 0, 0, None), + (STOCK_OVERWRITE, "_Overwrite", 0, 0, None), + (STOCK_PREVIOUS, "Pre_vious", 0, 0, None), + (STOCK_REMOVE, "Re_move", 0, 0, None) +)) + + + +class IconFactory(gtk.IconFactory): + + def __init__(self, widget): + gtk.IconFactory.__init__(self) + self.add_default() + + icons = { + STOCK_APPLICATION : "revelation.png", + STOCK_ACCOUNT_CREDITCARD : "account-creditcard.png", + STOCK_ACCOUNT_CRYPTOKEY : "account-cryptokey.png", + STOCK_ACCOUNT_DATABASE : "account-database.png", + STOCK_ACCOUNT_DOOR : "account-door.png", + STOCK_ACCOUNT_EMAIL : "account-email.png", + STOCK_ACCOUNT_FTP : "account-ftp.png", + STOCK_ACCOUNT_GENERIC : "account-generic.png", + STOCK_ACCOUNT_PHONE : "account-phone.png", + STOCK_ACCOUNT_SHELL : "account-shell.png", + STOCK_ACCOUNT_WEBSITE : "account-website.png", + STOCK_FOLDER : "folder.png", + STOCK_FOLDER_OPEN : "folder-open.png", + STOCK_LOCK : "account-generic.png", + STOCK_PASSWORD : "password.png" + } + + for id, filename in icons.items(): + iconset = gtk.IconSet(gtk.gdk.pixbuf_new_from_file(revelation.DATADIR + "/pixmaps/" + filename)) + self.add(id, iconset) + + itemicons = { + STOCK_ADD : STOCK_ACCOUNT, + STOCK_DISCARD : gtk.STOCK_DELETE, + STOCK_EDIT : gtk.STOCK_PROPERTIES, + STOCK_EXPORT : gtk.STOCK_EXECUTE, + STOCK_IMPORT : gtk.STOCK_CONVERT, + STOCK_LOCK : STOCK_LOCK, + STOCK_NEXT : gtk.STOCK_GO_FORWARD, + STOCK_OVERWRITE : gtk.STOCK_SAVE_AS, + STOCK_PREVIOUS : gtk.STOCK_GO_BACK, + STOCK_REMOVE : gtk.STOCK_DELETE + } + + for id, stock in itemicons.items(): + iconset = widget.get_style().lookup_icon_set(stock) + self.add(id, iconset) + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/ui.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/ui.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,619 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module containing specialized, high-level ui components +# +# +# Copyright (c) 2003-2004 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 gobject, gtk, gtk.gdk, gnome.ui, revelation, time, os, gconf + + +# this class handles the core application ui and mechanism, +# while functionality is handled by the main app +class App(gnome.ui.App): + + def __init__(self): + gnome.ui.App.__init__(self, revelation.APPNAME, revelation.APPNAME) + self.connect("file-changed", self.__cb_state_file) + os.umask(0077) + self.__init_facilities() + + # set up ui + self.set_default_size(600, 400) + self.set_icon_list( + gtk.gdk.pixbuf_new_from_file(revelation.DATADIR + "/pixmaps/revelation.png"), + gtk.gdk.pixbuf_new_from_file(revelation.DATADIR + "/pixmaps/revelation-16x16.png") + ) + + self.statusbar = gnome.ui.AppBar(gtk.FALSE, gtk.TRUE, gnome.ui.PREFERENCES_USER) + self.set_statusbar(self.statusbar) + + self.accelgroup = gtk.AccelGroup() + self.add_accel_group(self.accelgroup) + + self.__init_toolbar() + self.__init_menu() + self.__init_mainarea() + + # set up initial states + self.tree.select(None) + + self.if_menu.get_widget("
/Edit/Find Next").set_sensitive(gtk.FALSE) + self.if_menu.get_widget("
/Edit/Find Previous").set_sensitive(gtk.FALSE) + self.if_menu.get_widget("
/Edit/Undo").set_sensitive(gtk.FALSE) + self.if_menu.get_widget("
/Edit/Redo").set_sensitive(gtk.FALSE) + self.if_menu.get_widget("
/View/Show Passwords").set_active(self.gconf.get_bool("/apps/revelation/view/passwords")) + + self.show_all() + + if self.gconf.get_bool("/apps/revelation/view/toolbar") == gtk.FALSE: + self.get_dock_item_by_name("Toolbar").hide() + else: + self.if_menu.get_widget("
/View/Toolbar").set_active(gtk.TRUE) + + if self.gconf.get_bool("/apps/revelation/view/statusbar") == gtk.FALSE: + self.statusbar.hide() + else: + self.if_menu.get_widget("
/View/Statusbar").set_active(gtk.TRUE) + + self.file = None + self.password = None + self.filepassword = None + + + def __init_facilities(self): + self.icons = revelation.stock.IconFactory(self) + self.data = revelation.data.DataStore() + + self.gconf = gconf.client_get_default() + self.gconf.add_dir("/apps/revelation", gconf.CLIENT_PRELOAD_NONE) + self.gconf.notify_add("/apps/revelation/view/statusbar", self.__cb_gconf_statusbar) + self.gconf.notify_add("/apps/revelation/view/toolbar", self.__cb_gconf_toolbar) + self.gconf.notify_add("/apps/revelation/view/passwords", self.__cb_gconf_view_passwords) + + self.clipboard = revelation.data.EntryClipboard() + self.clipboard.connect("copy", self.__cb_state_clipboard) + self.clipboard.connect("cut", self.__cb_state_clipboard) + + self.undoqueue = revelation.data.UndoQueue() + self.undoqueue.connect("can-undo", self.__cb_state_undo, revelation.data.UNDO) + self.undoqueue.connect("can-redo", self.__cb_state_undo, revelation.data.REDO) + + self.finder = revelation.data.EntrySearch(self.data) + self.finder.connect("string_changed", self.__cb_state_find) + + + def __init_mainarea(self): + hpaned = gtk.HPaned() + hpaned.set_border_width(5) + hpaned.set_position(300) + self.set_contents(hpaned) + + self.tree = Tree(self.data) + self.tree.connect("button_press_event", self.__cb_popup_tree) + self.tree.selection.connect("changed", self.__cb_entry_display) + self.tree.selection.connect("changed", self.__cb_state_entry) + scrolledwindow = gtk.ScrolledWindow() + scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) + scrolledwindow.add(self.tree) + hpaned.pack1(scrolledwindow, gtk.TRUE) + + self.dataview = DataView() + self.dataview.display_info() + alignment = gtk.Alignment(0.5, 0.4, 0, 0) + alignment.add(self.dataview) + hpaned.pack2(alignment) + + + def __init_menu(self): + menuitems = ( + ("/_File", None, None, None, 0, ""), + ("/File/_New", "N", "Create a new file", None, 0, "", gtk.STOCK_NEW), + ("/File/_Open...", "O", "Open a file", None, 0, "", gtk.STOCK_OPEN), + ("/File/sep1", None, None, None, 0, ""), + ("/File/_Save", "S", "Save data to file", None, 0, "", gtk.STOCK_SAVE), + ("/File/Save _As...", "S", "Save data to different file", None, 0, "", gtk.STOCK_SAVE_AS), + ("/File/_Revert", None, "Revert to the saved copy of the file", None, 0, "", gtk.STOCK_REVERT_TO_SAVED), + ("/File/sep2", None, None, None, 0, ""), + ("/File/Change _Password...", None, "Change password of current file", None, 0, "", revelation.stock.STOCK_PASSWORD), + ("/File/_Lock...", "L", "Lock the current data file", None, 0, "", revelation.stock.STOCK_LOCK), + ("/File/sep3", None, None, None, 0, ""), + ("/File/_Import...", None, "Import data from a foreign file", None, 0, "", revelation.stock.STOCK_IMPORT), + ("/File/_Export...", None, "Export data to a different format", None, 0, "", revelation.stock.STOCK_EXPORT), + ("/File/sep4", None, None, None, 0, ""), + ("/File/_Close", "W", "Close the application", None, 0, "", gtk.STOCK_CLOSE), + ("/File/_Quit", "Q", "Quit the application", None, 0, "", gtk.STOCK_QUIT), + + ("/_Edit", None, None, None, 0, ""), + ("/Edit/_Add Entry...", "Insert", "Create a new entry", None, 0, "", revelation.stock.STOCK_ADD), + ("/Edit/_Edit", "Return", "Edit the selected entry", None, 0, "", revelation.stock.STOCK_EDIT), + ("/Edit/Re_move", "Delete", "Remove the selected entry", None, 0, "", revelation.stock.STOCK_REMOVE), + ("/Edit/sep1", None, None, None, 0, ""), + ("/Edit/_Undo", "Z", "Undo the last action", None, 0, "", gtk.STOCK_UNDO), + ("/Edit/_Redo", "Z", "Redo the previously undone action", None, 0, "", gtk.STOCK_REDO), + ("/Edit/sep2", None, None, None, 0, ""), + ("/Edit/Cu_t", "X", "Cut the entry to the clipboard", None, 0, "", gtk.STOCK_CUT), + ("/Edit/_Copy", "C", "Copy the entry to the clipboard", None, 0, "", gtk.STOCK_COPY), + ("/Edit/_Paste", "V", "Paste entry from clipboard", None, 0, "", gtk.STOCK_PASTE), + ("/Edit/sep3", None, None, None, 0, ""), + ("/Edit/_Find...", "F", "Search for an entry", None, 0, "", gtk.STOCK_FIND), + ("/Edit/Find Ne_xt", "G", "Find the next search match", None, 0, ""), + ("/Edit/Find Pre_vious", "G", "Find the previous search match", None, 0, ""), + ("/Edit/sep4", None, None, None, 0, ""), + ("/Edit/_Select All", "A", "Select all entries", lambda w,d: self.tree.select_all(), 0, ""), + ("/Edit/_Deselect All", "A", "Deselect all entries", lambda w,d: self.tree.unselect_all(), 0, ""), + ("/Edit/sep5", None, None, None, 0, ""), + ("/Edit/Prefere_nces", None, "Edit preferences", None, 0, "", gtk.STOCK_PREFERENCES), + + ("/_View", None, None, None, 0, ""), + ("/View/_Toolbar", None, "Toggle display of the toolbar", self.__cb_state_toolbar, 0, ""), + ("/View/_Statusbar", None, "Toggle display of the statusbar", self.__cb_state_statusbar, 0, ""), + ("/View/sep1", None, None, None, 0, ""), + ("/View/Show _Passwords", "P", "Show passwords", self.__cb_state_showpasswords, 0, ""), + + ("/_Help", None, None, None, 0, ""), + ("/Help/_Homepage", None, "Visit the Revelation homepage", lambda w,d: gnome.url_show(revelation.URL), 0, "", gtk.STOCK_HOME), + ("/Help/_About", None, "Show info about this application", lambda w,d: revelation.dialog.About().run(), 0, "", "gnome-stock-about") + ) + + self.if_menu = self.create_itemfactory(gtk.MenuBar, self.accelgroup, menuitems) + self.set_menus(self.if_menu.get_widget("
")) + + + def __init_toolbar(self): + self.toolbar = gtk.Toolbar() + self.toolbar.button_new = self.toolbar.insert_stock(gtk.STOCK_NEW, "New file", None, None, "", -1) + self.toolbar.button_open = self.toolbar.insert_stock(gtk.STOCK_OPEN, "Open file", None, None, "", -1) + self.toolbar.button_save = self.toolbar.insert_stock(gtk.STOCK_SAVE, "Save file", None, None, "", -1) + self.toolbar.append_space() + self.toolbar.button_entry_add = self.toolbar.insert_stock(revelation.stock.STOCK_ADD, "Add a new entry", None, None, "", -1) + self.toolbar.button_entry_edit = self.toolbar.insert_stock(revelation.stock.STOCK_EDIT, "Edit the selected entry", None, None, "", -1) + self.toolbar.button_entry_remove = self.toolbar.insert_stock(revelation.stock.STOCK_REMOVE, "Remove the selected entry", None, None, "", -1) + self.set_toolbar(self.toolbar) + + + def __setattr__(self, name, value): + if name == "file": + self.emit("file-changed", value) + + gnome.ui.App.__setattr__(self, name, value) + + + # gconf callbacks + def __cb_gconf_view_passwords(self, client, id, entry, data): + self.if_menu.get_widget("
/View/Show Passwords").set_active(entry.get_value().get_bool()) + + + def __cb_gconf_statusbar(self, client, id, entry, data): + if entry.get_value().get_bool() == gtk.TRUE: + self.statusbar.show() + self.if_menu.get_widget("
/View/Statusbar").set_active(gtk.TRUE) + else: + self.statusbar.hide() + self.if_menu.get_widget("
/View/Statusbar").set_active(gtk.FALSE) + + + def __cb_gconf_toolbar(self, client, id, entry, data): + if entry.get_value().get_bool() == gtk.TRUE: + self.get_dock_item_by_name("Toolbar").show() + self.if_menu.get_widget("
/View/Toolbar").set_active(gtk.TRUE) + else: + self.get_dock_item_by_name("Toolbar").hide() + self.if_menu.get_widget("
/View/Toolbar").set_active(gtk.FALSE) + + + # state callbacks + def __cb_state_clipboard(self, widget, data = None): + self.if_menu.get_widget("
/Edit/Paste").set_sensitive(self.clipboard.has_contents()) + + + def __cb_state_entry(self, widget, data = None): + selcount = len(self.tree.get_selected()) + + self.toolbar.button_entry_add.set_sensitive(selcount < 2) + self.if_menu.get_widget("
/Edit/Add Entry...").set_sensitive(selcount < 2) + self.if_menu.get_widget("
/Edit/Paste").set_sensitive(selcount < 2 and self.clipboard.has_contents()) + + self.toolbar.button_entry_edit.set_sensitive(selcount == 1) + self.if_menu.get_widget("
/Edit/Edit").set_sensitive(selcount == 1) + + self.toolbar.button_entry_remove.set_sensitive(selcount > 0) + self.if_menu.get_widget("
/Edit/Remove").set_sensitive(selcount > 0) + self.if_menu.get_widget("
/Edit/Cut").set_sensitive(selcount > 0) + self.if_menu.get_widget("
/Edit/Copy").set_sensitive(selcount > 0) + + + def __cb_state_file(self, object, data): + self.data.changed = gtk.FALSE + + self.if_menu.get_widget("
/File/Revert").set_sensitive(data != None) + self.if_menu.get_widget("
/File/Lock...").set_sensitive(data != None) + + if data == None: + self.set_title("[New file] - " + revelation.APPNAME) + self.password = None + self.filepassword = None + else: + self.set_title(os.path.basename(data) + " - " + revelation.APPNAME) + os.chdir(os.path.dirname(data)) + + + def __cb_state_find(self, widget, data = None): + self.if_menu.get_widget("
/Edit/Find Next").set_sensitive(data != "") + self.if_menu.get_widget("
/Edit/Find Previous").set_sensitive(data != "") + + + def __cb_state_showpasswords(self, object, data = None): + self.gconf.set_bool("/apps/revelation/view/passwords", self.if_menu.get_widget("
/View/Show Passwords").get_active()) + + + def __cb_state_statusbar(self, object, data): + active = self.if_menu.get_widget("
/View/Statusbar").get_active() + self.gconf.set_bool("/apps/revelation/view/statusbar", active) + + + def __cb_state_toolbar(self, object, data = None): + active = self.if_menu.get_widget("
/View/Toolbar").get_active() + self.gconf.set_bool("/apps/revelation/view/toolbar", active) + + + def __cb_state_undo(self, object, state, method): + if method == revelation.data.UNDO: + widget = self.if_menu.get_widget("
/Edit/Undo") + action = "_Undo" + elif method == revelation.data.REDO: + widget = self.if_menu.get_widget("
/Edit/Redo") + action = "_Redo" + + widget.set_sensitive(state) + item = widget.get_children()[0] + + if state == gtk.TRUE: + item.set_label(action + " " + self.undoqueue.get_data(method)["name"]) + else: + item.set_label(action) + + + # misc other callbacks + def __cb_entry_display(self, object, data = None): + self.dataview.display_entry(self.data.get_entry(self.tree.get_active())) + + + def __cb_menudesc(self, object, item, show): + if show: + self.statusbar.set_status(item.get_data("description")) + else: + self.statusbar.set_status("") + + + def __cb_popup_tree(self, widget, data = None): + if data.button != 3: + return + + path = self.tree.get_path_at_pos(int(data.x), int(data.y)) + + if path == None: + self.tree.unselect_all() + elif self.tree.selection.iter_is_selected(self.data.get_iter(path[0])) == gtk.FALSE: + self.tree.set_cursor(path[0], path[1], gtk.FALSE) + + iters = self.tree.get_selected() + + menuitems = [] + self.emit("tree-popup", menuitems, iters) + + itemfactory = self.create_itemfactory(gtk.Menu, gtk.AccelGroup(), menuitems) + itemfactory.popup(int(data.x_root), int(data.y_root), data.button, data.get_time()) + + return gtk.TRUE + + + # main methods + def create_itemfactory(self, widget, accelgroup, items): + itemfactory = MenuFactory(widget, accelgroup) + itemfactory.create_items(items) + itemfactory.connect("item-selected", self.__cb_menudesc, gtk.TRUE) + itemfactory.connect("item-deselected", self.__cb_menudesc, gtk.FALSE) + return itemfactory + + + def run(self): + self.show_all() + gtk.main() + + +gobject.signal_new("file-changed", App, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_PYOBJECT, )) +gobject.signal_new("tree-popup", App, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)) + + + +class DataView(gtk.VBox): + + def __init__(self): + gtk.VBox.__init__(self) + self.set_spacing(15) + self.set_border_width(10) + + self.size_name = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + self.size_value = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + + self.data = None + + + def clear(self, force = gtk.FALSE): + if force == gtk.TRUE or self.data is not None: + self.data = None + for child in self.get_children(): + child.destroy() + + + def display_entry(self, data): + if data is None: + self.clear() + return + + self.clear(gtk.TRUE) + self.data = data + + # set up metadata display + metabox = gtk.VBox() + metabox.set_spacing(4) + self.pack_start(metabox) + + metabox.pack_start(revelation.widget.ImageLabel( + data["icon"], revelation.stock.ICON_SIZE_DATAVIEW, + "" + revelation.misc.escape_markup(data["name"]) + "" + )) + + metabox.pack_start(revelation.widget.Label("" + revelation.entry.get_entry_data(data["type"], "name") + (data["description"] != "" and "; " or "") + "" + data["description"], gtk.JUSTIFY_CENTER)) + + # set up field list + rows = [] + + for field in revelation.entry.get_entry_fields(data["type"]): + if not data["fields"].has_key(field) or data["fields"][field] == "": + continue + + row = gtk.HBox() + row.set_spacing(5) + rows.append(row) + + label = revelation.widget.Label("" + revelation.misc.escape_markup(revelation.entry.get_field_data(field, "name")) + ":", gtk.JUSTIFY_RIGHT) + self.size_name.add_widget(label) + row.pack_start(label, gtk.FALSE, gtk.FALSE) + + + type = revelation.entry.get_field_data(field, "type") + + if type == revelation.entry.FIELD_TYPE_EMAIL: + widget = revelation.widget.HRef("mailto:" + data["fields"][field], data["fields"][field]) + + elif type == revelation.entry.FIELD_TYPE_URL: + widget = revelation.widget.HRef(data["fields"][field], data["fields"][field]) + + elif type == revelation.entry.FIELD_TYPE_PASSWORD: + widget = revelation.widget.PasswordLabel(data["fields"][field]) + + else: + widget = revelation.widget.Label(revelation.misc.escape_markup(data["fields"][field])) + widget.set_selectable(gtk.TRUE) + + self.size_value.add_widget(widget) + row.pack_start(widget, gtk.FALSE, gtk.FALSE) + + + if len(rows) > 0: + fieldlist = gtk.VBox() + fieldlist.set_spacing(2) + self.pack_start(fieldlist) + + for row in rows: + fieldlist.pack_start(row, gtk.FALSE, gtk.FALSE) + + # display updatetime + if data["type"] != revelation.entry.ENTRY_FOLDER: + self.pack_start(revelation.widget.Label("Updated " + revelation.misc.timediff_simple(data["updated"]) + " ago; \n" + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(data["updated"])), gtk.JUSTIFY_CENTER)) + + self.show_all() + + + def display_info(self): + self.clear(gtk.TRUE) + + self.pack_start(revelation.widget.ImageLabel( + revelation.stock.STOCK_APPLICATION, gtk.ICON_SIZE_DND, + "" + revelation.APPNAME + " " + revelation.VERSION + "" + )) + + self.pack_start(revelation.widget.Label("A password manager for GNOME 2")) + + gpl = "\nThis 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.\n\nThis 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." + label = revelation.widget.Label("" + gpl + "", gtk.JUSTIFY_LEFT) + label.set_size_request(250, -1) + self.pack_start(label) + + self.show_all() + + + def pack_start(self, widget): + alignment = gtk.Alignment(0.5, 0.5, 0, 0) + alignment.add(widget) + gtk.VBox.pack_start(self, alignment) + + + +class MenuFactory(gtk.ItemFactory): + + def __init__(self, widget, accelgroup): + gtk.ItemFactory.__init__(self, widget, "
", accelgroup) + + def __cb_select(self, object): + self.emit("item-selected", object) + + def __cb_deselect(self, object): + self.emit("item-deselected", object) + + + def create_items(self, items): + + # strip description from items, and create the items + ifitems = [] + for item in items: + ifitems.append(item[0:2] + item[3:]) + + gtk.ItemFactory.create_items(self, ifitems) + + # set up description for items + for item in items: + if item[5] in ["", "", ""]: + widget = self.get_widget("
" + item[0].replace("_", "")) + widget.set_data("description", item[2]) + widget.connect("select", self.__cb_select) + widget.connect("deselect", self.__cb_deselect) + + +gobject.signal_new("item-selected", MenuFactory, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gtk.MenuItem,)) +gobject.signal_new("item-deselected", MenuFactory, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gtk.MenuItem,)) + + + +class Tree(gtk.TreeView): + + def __init__(self, datastore = None): + gtk.TreeView.__init__(self, datastore) + self.set_headers_visible(gtk.FALSE) + self.model = datastore + self.selection = self.get_selection() + self.selection.set_mode(gtk.SELECTION_MULTIPLE) + + self.connect("button_press_event", self.__cb_buttonpress) + self.connect("key_press_event", self.__cb_keypress) + self.connect("row-expanded", self.__cb_row_expanded) + self.connect("row-collapsed", self.__cb_row_collapsed) + + column = gtk.TreeViewColumn() + self.append_column(column) + + cr = gtk.CellRendererPixbuf() + column.pack_start(cr, gtk.FALSE) + column.add_attribute(cr, "stock-id", revelation.data.ENTRYSTORE_COL_ICON) + cr.set_property("stock-size", revelation.stock.ICON_SIZE_TREEVIEW) + + cr = gtk.CellRendererText() + column.pack_start(cr, gtk.TRUE) + column.add_attribute(cr, "text", revelation.data.ENTRYSTORE_COL_NAME) + + + def __cb_buttonpress(self, object, data): + if data.button == 1 and data.type == gtk.gdk._2BUTTON_PRESS: + path = self.get_path_at_pos(int(data.x), int(data.y)) + + if path != None: + iter = self.model.get_iter(path[0]) + self.toggle_expanded(iter) + self.emit("doubleclick", iter) + + def __cb_keypress(self, object, data): + if data.keyval == 32: + self.toggle_expanded(self.get_active()) + + def __cb_row_collapsed(self, object, iter, extra): + self.model.set_folder_state(iter, gtk.FALSE) + + def __cb_row_expanded(self, object, iter, extra): + for i in range(self.model.iter_n_children(iter)): + child = self.model.iter_nth_child(iter, i) + if self.row_expanded(self.model.get_path(child)) == gtk.FALSE: + self.model.set_folder_state(child, gtk.FALSE) + + self.model.set_folder_state(iter, gtk.TRUE) + + + def collapse_row(self, iter): + gtk.TreeView.collapse_row(self, self.model.get_path(iter)) + + + def expand_row(self, iter): + if iter is not None and self.model.iter_n_children(iter) > 0: + gtk.TreeView.expand_row(self, self.model.get_path(iter), gtk.FALSE) + + + def expand_to_iter(self, iter): + path = self.model.get_path(iter) + for i in range(len(path)): + iter = self.model.get_iter(path[0:i]) + self.expand_row(iter) + + + def get_active(self): + iter = self.model.get_iter(self.get_cursor()[0]) + + if iter == None or self.selection.iter_is_selected(iter) == gtk.FALSE: + return None + + return iter + + + def get_selected(self): + list = [] + self.selection.selected_foreach(lambda model, path, iter: list.append(iter)) + return list + + + def select(self, iter): + if iter == None: + self.unselect_all() + else: + self.expand_to_iter(iter) + self.set_cursor(self.model.get_path(iter)) + + + def select_all(self): + self.selection.select_all() + self.selection.emit("changed") + self.emit("cursor_changed") + + + def set_model(self, model): + gtk.TreeView.set_model(self, model) + self.model = model + + if model is not None: + for i in range(model.iter_n_children(None)): + model.set_folder_state(model.iter_nth_child(None, i), gtk.FALSE) + + + def toggle_expanded(self, iter): + if iter == None: + return + elif self.row_expanded(self.model.get_path(iter)): + self.collapse_row(iter) + else: + self.expand_row(iter) + + + def unselect_all(self): + self.selection.unselect_all() + self.selection.emit("changed") + self.emit("cursor_changed") + self.emit("unselect_all") + +gobject.signal_new("doubleclick", Tree, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, (gobject.TYPE_PYOBJECT, )) + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/lib/widget.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/lib/widget.py Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,370 @@ +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Module containing custom widgets +# +# +# Copyright (c) 2003-2004 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 gobject, gtk, gtk.gdk, gnome.ui, revelation, gconf, os.path + + +class GConfHandler: + """Abstract class which handles synchronization between + a widget and a gconf key""" + + def __cb_get(self, client, id, entry, data): + self.gconf_cb_get(self.gconf, self.gconf_key) + + def __cb_set(self, object, data = None): + self.gconf_cb_set(self.gconf, self.gconf_key) + + def __cb_unrealize(self, object, data = None): + self.gconf.notify_remove(self.gconf_conn) + + def gconf_bind(self, key, cb_get, cb_set, setsignal): + self.gconf = gconf.client_get_default() + self.gconf_key = key + self.gconf_cb_get = cb_get + self.gconf_cb_set = cb_set + + cb_get(self.gconf, self.gconf_key) + self.gconf_conn = self.gconf.notify_add(key, self.__cb_get) + self.connect("unrealize", self.__cb_unrealize) + + if cb_set != None and setsignal != None: + self.connect(setsignal, self.__cb_set) + + +# first, some simple subclasses - replacements for gtk widgets +class CheckButton(gtk.CheckButton, GConfHandler): + + def __init__(self, label = None): + gtk.CheckButton.__init__(self, label) + + def __cb_gconf_get(self, client, key): + self.set_active(client.get_bool(key)) + + def __cb_gconf_set(self, client, key): + client.set_bool(key, self.get_active()) + + def gconf_bind(self, key): + GConfHandler.gconf_bind(self, key, self.__cb_gconf_get, self.__cb_gconf_set, "toggled") + + + +class Entry(gtk.Entry): + + def __init__(self, text = None): + gtk.Entry.__init__(self) + self.set_activates_default(gtk.TRUE) + self.set_text(text) + + def set_text(self, text): + if text == None: + text = "" + + gtk.Entry.set_text(self, text) + + + +class FileEntry(gnome.ui.FileEntry, GConfHandler): + + def __init__(self, history, title): + gnome.ui.FileEntry.__init__(self, history, title) + self.set_modal(gtk.TRUE) + + def __cb_gconf_get(self, client, key): + self.gtk_entry().set_text(client.get_string(key)) + + def __cb_gconf_set(self, client, key): + client.set_string(key, self.gtk_entry().get_text()) + + def gconf_bind(self, key): + GConfHandler.gconf_bind(self, key, self.__cb_gconf_get, self.__cb_gconf_set, "changed") + + def get_filename(self): + return gnome.ui.FileEntry.get_full_path(self, gtk.FALSE) + + def set_filename(self, filename): + self.gtk_entry().set_text(filename) + + + +class HRef(gnome.ui.HRef): + + def __init__(self, url, text): + gnome.ui.HRef.__init__(self, url, text) + self.get_children()[0].set_alignment(0, 0.5) + + + +class ImageMenuItem(gtk.ImageMenuItem): + + def __init__(self, stock, text = None): + gtk.ImageMenuItem.__init__(self, stock) + + self.label = self.get_children()[0] + self.image = self.get_children()[1] + + if text is not None: + self.set_text(text) + + def set_stock(self, stock): + self.image.set_from_stock(stock, gtk.ICON_SIZE_MENU) + + def set_text(self, text): + self.label.set_text(text) + + + +class Label(gtk.Label): + + def __init__(self, text = None, justify = gtk.JUSTIFY_LEFT): + gtk.Label.__init__(self, text) + + self.set_use_markup(gtk.TRUE) + self.set_line_wrap(gtk.TRUE) + + self.set_justify(justify) + + if justify == gtk.JUSTIFY_LEFT: + self.set_alignment(0, 0.5) + + elif justify == gtk.JUSTIFY_CENTER: + self.set_alignment(0.5, 0.5) + + elif justify == gtk.JUSTIFY_RIGHT: + self.set_alignment(1, 0.5) + + + +class OptionMenu(gtk.OptionMenu): + + def __init__(self, menu = None): + gtk.OptionMenu.__init__(self) + + if menu == None: + menu = gtk.Menu() + + self.set_menu(menu) + + def append_item(self, item): + self.menu.append(item) + if len(self.menu.get_children()) == 1: + self.set_history(0) + + def get_active_item(self): + return self.menu.get_children()[self.get_history()] + + def get_item(self, index): + items = self.menu.get_children() + return index < len(items) and items[index] or None + + def set_active_item(self, activeitem): + items = self.get_menu().get_children() + + for i, item in zip(range(len(items)), items): + if activeitem == item: + self.set_history(i) + + def set_menu(self, menu): + self.menu = menu + gtk.OptionMenu.set_menu(self, menu) + + + +class SpinButton(gtk.SpinButton, GConfHandler): + + def __init__(self, adjustment = None, climb_rate = 0.0, digits = 0): + gtk.SpinButton.__init__(self, adjustment, climb_rate, digits) + self.set_increments(1, 1) + self.set_numeric(gtk.TRUE) + + def __cb_gconf_get(self, client, key): + self.set_value(client.get_int(key)) + + def __cb_gconf_set(self, client, key): + client.set_int(key, self.get_value_as_int()) + + def gconf_bind(self, key): + GConfHandler.gconf_bind(self, key, self.__cb_gconf_get, self.__cb_gconf_set, "changed") + + + +# more extensive custom widgets +class EntryDropdown(OptionMenu): + + def __init__(self): + revelation.widget.OptionMenu.__init__(self) + + typelist = revelation.entry.get_entry_list() + typelist.remove(revelation.entry.ENTRY_FOLDER) + typelist.insert(0, revelation.entry.ENTRY_FOLDER) + + for type in typelist: + item = revelation.widget.ImageMenuItem(revelation.entry.get_entry_data(type, "icon"), revelation.entry.get_entry_data(type, "name")) + item.type = type + self.append_item(item) + + if type == revelation.entry.ENTRY_FOLDER: + self.append_item(gtk.SeparatorMenuItem()) + + + def get_type(self): + item = self.get_active_item() + return hasattr(item, "type") and item.type or None + + + def set_type(self, type): + for item in self.get_menu().get_children(): + if hasattr(item, "type") and item.type == type: + self.set_active_item(item) + + + +class ImageLabel(gtk.Alignment): + + def __init__(self, stock, size, text): + gtk.Alignment.__init__(self, 0.5, 0.5, 0, 0) + + self.hbox = gtk.HBox() + self.hbox.set_spacing(5) + self.add(self.hbox) + + self.image = gtk.Image() + self.hbox.pack_start(self.image, gtk.FALSE, gtk.FALSE) + self.set_stock(stock, size) + + self.label = Label(text, gtk.JUSTIFY_CENTER) + self.hbox.pack_start(self.label) + + def set_stock(self, stock, size): + self.image.set_from_stock(stock, size) + + def set_text(self, text): + self.label.set_text(text) + + + +class InputSection(gtk.VBox): + + def __init__(self, title = None, sizegroup = None, description = None): + gtk.VBox.__init__(self) + self.set_border_width(0) + self.set_spacing(6) + + self.title = None + self.description = None + self.sizegroup = sizegroup is not None and sizegroup or gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) + + if title is not None: + self.title = Label("" + revelation.misc.escape_markup(title) + "") + self.pack_start(self.title, gtk.FALSE) + + if description is not None: + self.description = Label(revelation.misc.escape_markup(description)) + self.pack_start(self.description, gtk.FALSE) + + + def add_inputrow(self, title, widget): + row = gtk.HBox() + row.set_spacing(6) + self.pack_start(row) + + if self.title is not None: + row.pack_start(Label(" "), gtk.FALSE, gtk.FALSE) + + if title is not None: + label = Label(revelation.misc.escape_markup(title) + ":") + self.sizegroup.add_widget(label) + row.pack_start(label, gtk.FALSE, gtk.FALSE) + + row.pack_start(widget) + + return row + + + def clear(self): + for child in self.get_children(): + if child not in [ self.label, self.description ]: + child.destroy() + + + +class PasswordEntry(gtk.HBox, GConfHandler): + + def __init__(self, value = None, generator = gtk.TRUE, ignorehide = gtk.FALSE): + gtk.HBox.__init__(self) + + self.entry = revelation.widget.Entry(value) + self.entry.connect("changed", self.__cb_changed) + self.pack_start(self.entry) + + if ignorehide == gtk.FALSE: + self.gconf_bind("/apps/revelation/view/passwords", self.__cb_gconf_password, None, None) + + if generator == gtk.TRUE: + self.pwgen = gtk.Button("Generate") + self.pwgen.connect("clicked", self.__cb_generate) + self.pack_start(self.pwgen, gtk.FALSE, gtk.FALSE) + + + def __cb_changed(self, widget, data = None): + self.emit("changed") + + def __cb_gconf_password(self, client, key): + self.set_visibility(client.get_bool(key)) + + def __cb_generate(self, object, data = None): + self.entry.set_text(revelation.misc.generate_password()) + + + def get_text(self): + return self.entry.get_text() + + def set_activates_default(self, activates): + self.entry.set_activates_default(activates) + + def set_text(self, text): + self.entry.set_text(text) + + def set_visibility(self, visibility): + self.entry.set_visibility(visibility) + +gobject.signal_new("changed", PasswordEntry, gobject.SIGNAL_ACTION, gobject.TYPE_BOOLEAN, ()) + + + +class PasswordLabel(Label, GConfHandler): + + def __init__(self, password, justify = gtk.JUSTIFY_LEFT): + Label.__init__(self, password, justify) + self.password = password + + self.gconf_bind("/apps/revelation/view/passwords", self.__cb_gconf_password, None, None) + + def __cb_gconf_password(self, client, key): + if client.get_bool(key) == gtk.TRUE: + Label.set_text(self, self.password) + self.set_selectable(gtk.TRUE) + else: + Label.set_text(self, "******") + self.set_selectable(gtk.FALSE) + diff -r 0000000000000000000000000000000000000000 -r cea4127a11c0e373fb731e097f639175c263faab src/revelation --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/revelation Mon Apr 26 21:34:11 2004 +0000 @@ -0,0 +1,645 @@ +#!/usr/bin/env python + +# +# Revelation 0.3.0 - a password manager for GNOME 2 +# http://oss.wired-networks.net/revelation/ +# +# Copyright (c) 2003-2004 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 pygtk +pygtk.require("2.0") +import gtk, gnome, gnome.ui, revelation, os, os.path, sys, gobject, gc, gconf + +class Revelation(revelation.ui.App): + + def __init__(self): + revelation.ui.App.__init__(self) + + self.toolbar.button_new.connect("clicked", lambda w: self.file_new()) + self.toolbar.button_open.connect("clicked", lambda w: self.file_open()) + self.toolbar.button_save.connect("clicked", lambda w: self.file_save(self.file, self.password)) + self.toolbar.button_entry_add.connect("clicked", lambda w: self.entry_add()) + self.toolbar.button_entry_edit.connect("clicked", lambda w: self.entry_edit()) + self.toolbar.button_entry_remove.connect("clicked", lambda w: self.entry_remove()) + + menucb = { + "File/New" : lambda w: self.file_new(), + "File/Open..." : lambda w: self.file_open(), + "File/Save" : lambda w: self.file_save(self.file, self.password), + "File/Save As..." : lambda w: self.file_save(None, None), + "File/Revert" : lambda w: self.file_revert(), + "File/Change Password..." : lambda w: self.change_password(), + "File/Lock..." : lambda w: self.file_lock(), + "File/Import..." : lambda w: self.file_import(), + "File/Export..." : lambda w: self.file_export(), + "File/Close" : lambda w: self.__cb_quit(), + "File/Quit" : lambda w: self.__cb_quit(), + + "Edit/Add Entry..." : lambda w: self.entry_add(), + "Edit/Edit" : lambda w: self.entry_edit(), + "Edit/Remove" : lambda w: self.entry_remove(), + "Edit/Undo" : lambda w: self.undoqueue.undo(), + "Edit/Redo" : lambda w: self.undoqueue.redo(), + "Edit/Cut" : lambda w: self.clip_cut(), + "Edit/Copy" : lambda w: self.clip_copy(), + "Edit/Paste" : lambda w: self.clip_paste(), + "Edit/Find..." : lambda w: self.entry_find(), + "Edit/Find Next" : lambda w: self.__entry_find(self, revelation.data.SEARCH_NEXT), + "Edit/Find Previous" : lambda w: self.__entry_find(self, revelation.data.SEARCH_PREV), + "Edit/Preferences" : lambda w: revelation.dialog.Preferences(self).run() + } + + for path, cb in menucb.items(): + self.if_menu.get_widget("
/" + path).connect("activate", cb) + + self.connect("delete_event", self.__cb_quit) + self.connect("tree-popup", self.__cb_popup_tree) + self.undoqueue.connect("undo", self.__cb_undo, revelation.data.UNDO) + self.undoqueue.connect("redo", self.__cb_undo, revelation.data.REDO) + self.tree.connect("doubleclick", self.__cb_doubleclick_tree) + + + def __cb_doubleclick_tree(self, widget, iter): + if self.data.get_entry_type(iter) != revelation.entry.ENTRY_FOLDER: + self.entry_edit() + + + def __cb_popup_tree(self, object, menuitems, iters): + + if len(iters) == 1: + menuitems.append(("/Edit", None, "Edit the selected entry", lambda w,d: self.entry_edit(), 0, "", revelation.stock.STOCK_EDIT)) + + if len(iters) > 0: + menuitems.append(("/Remove", None, "Remove the selected entry", lambda w,d: self.entry_remove(), 0, "", revelation.stock.STOCK_REMOVE)) + + if len(iters) < 2: + menuitems.append(("/Add Entry...", None, "Create a new entry", lambda w,d: self.entry_add(), 0, "", revelation.stock.STOCK_ADD)) + + # set up clipboard menu + clipboardmenu = [] + + if len(iters) > 0: + clipboardmenu.append(("/Cut", "", "Cut the selected entry to the clipboard", lambda w,d: self.clip_cut(), 0, "", gtk.STOCK_CUT)) + clipboardmenu.append(("/Copy", "", "Copy the selected entry to the keyboard", lambda w,d: self.clip_copy(), 0, "", gtk.STOCK_COPY)) + + if len(iters) < 2 and self.clipboard.has_contents(): + clipboardmenu.append(("/Paste", "", "Paste entry from clipboard", lambda w,d: self.clip_paste(), 0, "", gtk.STOCK_PASTE)) + + if len(clipboardmenu) > 0: + menuitems.append(("/sep1", None, None, None, 0, "")) + menuitems.extend(clipboardmenu) + + + def __cb_quit(self, object = None, data = None): + return gtk.TRUE ^ self.quit() + + + def __cb_undo(self, object, data, method): + self.undo(data["name"], data["action"], data["data"], method) + + + def __entry_find(self, parent, direction = revelation.data.SEARCH_NEXT): + self.finder.folders = self.gconf.get_bool("/apps/revelation/search/folders") + self.finder.casesens = self.gconf.get_bool("/apps/revelation/search/casesens") + self.finder.namedesc = self.gconf.get_bool("/apps/revelation/search/namedesc") + + match = self.finder.find(self.tree.get_active(), direction) + + if match == None: + revelation.dialog.Error(parent, "No match found", "The string you searched for did not match any entries. Try using a different search-phrase.").run() + else: + self.tree.select(match) + + + def change_password(self): + try: + dialog = revelation.dialog.Password( + self, "Enter new password", + "Enter a new password for the current data file. The file must be saved before the new password is applied.", + 32, self.password != None, gtk.TRUE + ) + + while 1: + dialog.run() + + if self.password != None and dialog.entry_password.get_text() != self.password: + revelation.dialog.Error(dialog, "Incorrect password", "The password you entered as the current file password is incorrect.").run() + + else: + self.password = dialog.entry_new.get_text() + self.data.changed = gtk.TRUE + self.statusbar.set_status("Password changed") + break + + except revelation.CancelError: + self.statusbar.set_status("Password change cancelled") + + dialog.destroy() + + + def clip_copy(self): + iters = self.data.filter_parents(self.tree.get_selected()) + self.clipboard.copy(self.data, iters) + + + def clip_cut(self): + iters = self.data.filter_parents(self.tree.get_selected()) + self.undo_add_action(self.clip_cut, iters) + self.clipboard.cut(self.data, iters) + self.tree.unselect_all() + + + def clip_paste(self): + if self.clipboard.is_empty(): + return + + iter = self.tree.get_active() + + if self.data.get_entry_type(iter) in [ revelation.entry.ENTRY_FOLDER, None ]: + parent = iter + sibling = None + else: + parent = self.data.iter_parent(iter) + sibling = iter + + iters = self.clipboard.paste(self.data, parent, sibling) + self.undo_add_action(self.clip_paste, iters) + self.tree.select(iters[0]) + + + def entry_add(self): + try: + data = revelation.dialog.EditEntry(self, "Add entry").run() + iter = self.data.add_entry(self.tree.get_active(), data) + self.undo_add_action(self.entry_add, iter) + self.tree.select(iter) + self.statusbar.set_status("Added entry '" + data["name"] + "'") + + except revelation.CancelError: + self.statusbar.set_status("Add entry cancelled") + + + def entry_edit(self): + iter = self.tree.get_active() + + if iter == None: + return + + try: + data = self.data.get_entry(iter) + dialog = revelation.dialog.EditEntry(self, "Edit entry", data) + + if data["type"] == revelation.entry.ENTRY_FOLDER and self.data.iter_n_children(iter) > 0: + dialog.set_typechange_allowed(gtk.FALSE) + + newdata = dialog.run() + self.data.update_entry(iter, newdata) + self.undo_add_action(self.entry_edit, iter, data) + self.tree.select(iter) + self.statusbar.set_status("Updated entry '" + newdata["name"] + "'") + + except revelation.CancelError: + self.statusbar.set_status("Update entry cancelled") + + + def entry_find(self): + dialog = revelation.dialog.Find(self) + dialog.entry_phrase.set_text(self.finder.string) + dialog.dropdown.set_type(self.finder.type) + + while 1: + response = dialog.run() + self.finder.string = dialog.entry_phrase.get_text() + self.finder.type = dialog.dropdown.get_type() + + if response == revelation.dialog.RESPONSE_NEXT: + self.__entry_find(dialog, revelation.data.SEARCH_NEXT) + + elif response == revelation.dialog.RESPONSE_PREVIOUS: + self.__entry_find(dialog, revelation.data.SEARCH_PREV) + + else: + dialog.destroy() + break + + + def entry_remove(self): + iters = self.tree.get_selected() + + if len(iters) == 0: + return + + elif len(iters) == 1: + data = self.data.get_entry(iters[0]) + + if data["type"] == revelation.entry.ENTRY_FOLDER: + pritext = "Really remove folder '" + data["name"] + "'?" + sectext = "By removing this folder you will also remove all accounts and folders it contains." + + else: + pritext = "Really remove account '" + data["name"] + "'?" + sectext = "Please confirm that you wish to remove this account." + + statustext = "Removed entry '" + data["name"] + "'" + + else: + pritext = "Really remove the " + str(len(iters)) + " selected entries?" + sectext = "By removing these entries you will also remove any entries they may contain." + statustext = "Removed " + str(len(iters)) + " entries" + + + if revelation.dialog.RemoveEntry(self, pritext, sectext).run() == gtk.TRUE: + iters = self.data.filter_parents(iters) + self.undo_add_action(self.entry_remove, iters) + + for iter in iters: + self.data.remove_entry(iter) + + self.tree.unselect_all() + self.statusbar.set_status(statustext) + + else: + self.statusbar.set_status("Remove entry cancelled") + + + def file_export(self): + try: + + # disable garbage collection, to work around pygtk bug 122569 + gc.disable() + datafile = revelation.druid.ExportFile(self).run() + gc.enable() + + datafile.save(self.data) + + except revelation.CancelError: + self.statusbar.set_status("Export cancelled") + + except IOError: + revelation.dialog.Error(self, "Unable to write to file", "The file '" + datafile.file + "' could not be opened for writing. Make sure that you have the proper permissions to write to it.").run() + self.statusbar.set_status("Export failed") + + else: + self.statusbar.set_status("Data exported to " + datafile.file) + + + + def file_import(self): + try: + + # disable garbage collection, to work around pygtk bug 122569 + gc.disable() + datafile = revelation.druid.ImportFile(self).run() + gc.enable() + + entrystore = datafile.load() + + except IOError: + revelation.dialog.Error(self, "Unable to open file", "The file '" + datafile.file + "' could not be opened. Make sure that the file exists, and that you have the proper permissions to open it.").run() + self.statusbar.set_status("Import failed") + + except revelation.datafile.FormatError: + revelation.dialog.Error(self, "Invalid file format", "The file '" + datafile.file + "' is not a valid data file.").run() + self.statusbar.set_status("Import failed") + + except (revelation.datafile.EntryTypeError, revelation.datafile.EntryFieldError): + revelation.dialog.Error(self, "Unknown data", "The file '" + datafile.file + "' contained unknown data items. It may have been created by a future version of Revelation, try upgrading.").run() + self.statusbar.set_status("Import failed") + + except revelation.datafile.PasswordError: + revelation.dialog.Error(self, "Wrong password", "You entered a wrong password for the file '" + datafile.file + "'").run() + self.statusbar.set_status("Import failed") + + except revelation.datafile.VersionError: + revelation.dialog.Error(self, "Unknown data version", "The file '" + datafile.file + "' has a future version number - upgrade Revelation to a more recent version to open it.").run() + self.statusbar.set_status("Import failed") + + except revelation.CancelError: + self.statusbar.set_status("Import cancelled") + self.statusbar.set_status("Import failed") + + else: + iters = self.data.import_entrystore(entrystore) + self.undo_add_action(self.file_import, iters) + self.statusbar.set_status("Data imported from " + datafile.file) + + + def file_lock(self): + if self.password == None: + return + + iter = self.tree.get_active() + self.tree.set_model(None) + self.dataview.clear() + self.statusbar.set_status("File locked") + + dialog = revelation.dialog.Password(self, "Enter password to unlock file", "The current file has been locked. Please enter the file password to unlock it.") + dialog.get_button(1).destroy() + + while 1: + try: + dialog.run() + + if dialog.entry_password.get_text() != self.password: + revelation.dialog.Error(dialog, "Incorrect password", "The password you entered was not correct. Please try again.").run() + else: + break + + except revelation.CancelError: + pass + + dialog.destroy() + + self.statusbar.set_status("File unlocked") + self.tree.set_model(self.data) + self.tree.select(iter) + + + def file_new(self): + if not self.save_changes("Save changes to current file?", "You have made changes which have not been saved. If you create a new file without saving, then these changes will be discarded."): + self.statusbar.set_status("New file cancelled") + return gtk.FALSE + + self.data.clear() + self.dataview.clear() + self.undoqueue.clear() + self.file = None + self.statusbar.set_status("New file created") + + + def file_open(self, file = None, password = None, ignorechanges = gtk.FALSE): + dialog = None + + try: + if ignorechanges == gtk.FALSE and not self.save_changes("Save changes before opening?", "You have made changes which have not been saved. If you open another file without saving, then these changes will be discarded."): + return gtk.FALSE + + if file == None: + file = revelation.dialog.FileSelector("Select file to open").run() + + datafile = revelation.datafile.DataFile(file, revelation.datafile.TYPE_REVELATION) + datafile.check_file() + datafile.check_format() + + while 1: + try: + if password is None: + if dialog is None: + dialog = revelation.dialog.Password(self, "Enter file password", "The file '" + file + "' is encrypted. Please enter the file password to open it.", datafile.keysize) + + dialog.run() + password = dialog.entry_password.get_text() + + datafile.password = password + entrystore = datafile.load() + + except revelation.datafile.PasswordError: + password = None + revelation.dialog.Error(dialog is None and self or dialog, "Incorrect password", "The password you entered for the file '" + datafile.file + "' was not correct.").run() + + else: + break + + except revelation.CancelError: + self.statusbar.set_status("Open cancelled") + + except revelation.datafile.FormatError: + self.statusbar.set_status("Open failed") + revelation.dialog.Error(self, "Invalid file format", "The file '" + file + "' is not a valid data file.").run() + + except (revelation.datafile.EntryTypeError, revelation.datafile.EntryFieldError): + self.statusbar.set_status("Open failed") + revelation.dialog.Error(self, "Unknown data", "The file '" + datafile.file + "' contained unknown data items. It may have been created by a future version of Revelation, try upgrading.").run() + + except revelation.datafile.VersionError: + self.statusbar.set_status("Open failed") + revelation.dialog.Error(self, "Unknown data version", "The file '" + file + "' has a future version number - upgrade Revelation to a more recent version to open it.").run() + + except IOError: + self.statusbar.set_status("Open failed") + revelation.dialog.Error(self, "Unable to open file", "The file '" + file + "' could not be opened. Make sure that the file exists, and that you have the proper permissions to open it.").run() + + else: + self.data.clear() + self.undoqueue.clear() + self.data.import_entrystore(entrystore) + + self.file = file + self.password = password + self.filepassword = password + self.statusbar.set_status("Opened file " + file) + + if dialog is not None: + dialog.destroy() + + + def file_revert(self): + if self.data.changed == gtk.TRUE and revelation.dialog.Hig( + self, "Ignore unsaved changes?", "You have made changes which have not yet been saved. If you revert to the saved file then these changes will be lost.", + gtk.STOCK_DIALOG_WARNING, [ [ gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL ], [ gtk.STOCK_REVERT_TO_SAVED, gtk.RESPONSE_OK ] ], 0 + ).run() != gtk.RESPONSE_OK: + return gtk.FALSE + + self.file_open(self.file, self.filepassword, gtk.TRUE) + + + def file_save(self, file = None, password = None): + try: + if file == None: + file = revelation.dialog.FileSelector("Select file to save data to").run() + + if file != self.file and os.access(file, os.F_OK) == 1 and revelation.dialog.FileOverwrite(self, file).run() == gtk.FALSE: + raise revelation.CancelError + + datafile = revelation.datafile.DataFile(file, revelation.datafile.TYPE_REVELATION) + + if password == None: + try: + dialog = revelation.dialog.Password( + self, "Enter file password", + "Please enter a password which will be used to encrypt the file. You will need this password to open the file at a later time.", + datafile.keysize, gtk.FALSE, gtk.TRUE + ) + + dialog.run() + password = dialog.entry_new.get_text() + dialog.destroy() + + except revelation.CancelError: + dialog.destroy() + raise revelation.CancelError + + datafile.password = password + datafile.save(self.data) + + except revelation.CancelError: + self.statusbar.set_status("Save cancelled") + return gtk.FALSE + + except IOError: + revelation.dialog.Error(self, "Unable to write to file", "The file '" + datafile.file + "' could not be opened for writing. Make sure that you have the proper permissions to write to it.").run() + self.statusbar.set_status("Save failed") + return gtk.FALSE + + else: + self.file = file + self.password = password + self.filepassword = password + self.statusbar.set_status("Data saved to file " + file) + return gtk.TRUE + + + def save_changes(self, pritext, sectext): + if self.data.changed == gtk.FALSE: + return gtk.TRUE + + try: + if revelation.dialog.SaveChanges(self, pritext, sectext).run() == gtk.TRUE and self.file_save() == gtk.FALSE: + return gtk.FALSE + + except revelation.CancelError: + return gtk.FALSE + + return gtk.TRUE + + + def quit(self): + if not self.save_changes("Save changes before quitting?", "You have made changes which have not been saved. If you quit without saving, then these changes will be discarded."): + self.statusbar.set_status("Quit cancelled") + return gtk.FALSE + + gtk.mainquit() + return gtk.TRUE + + + def run(self, file = None): + if file != None: + self.file_open(file) + elif self.gconf.get_bool("/apps/revelation/file/autoload"): + self.file_open(self.gconf.get_string("/apps/revelation/file/autoload_file")) + + gtk.main() + + + def undo(self, name, action, data, method = revelation.data.UNDO): + iters = [] + + # handle add, paste and import actions + if action == self.entry_add or action == self.clip_paste or action == self.file_import: + + if method == revelation.data.UNDO: + for item in data: + item["iter"] = self.data.get_iter(item["path"]) + + for item in data: + self.data.remove_entry(item["iter"]) + + elif method == revelation.data.REDO: + for item in data: + newiters = self.data.import_entrystore_before(item["data"], self.data.get_iter(item["parent"]), self.data.get_iter(item["path"])) + iters.extend(newiters) + + + # handle remove and cut actions + elif action == self.entry_remove or action == self.clip_cut: + + if method == revelation.data.UNDO: + for item in data: + newiters = self.data.import_entrystore_before(item["data"], self.data.get_iter(item["parent"]), self.data.get_iter(item["path"])) + iters.extend(newiters) + + + elif method == revelation.data.REDO: + for item in data: + item["iter"] = self.data.get_iter(item["path"]) + + for item in data: + self.data.remove_entry(item["iter"]) + + + # handle edit action + elif action == self.entry_edit: + + iter = self.data.get_iter(data[0]["path"]) + iters.append(iter) + + if method == revelation.data.UNDO: + self.data.update_entry(iter, data[0]["predata"]) + + elif method == revelation.data.REDO: + self.data.update_entry(iter, data[0]["data"]) + + + # update status + if method == revelation.data.UNDO: + self.statusbar.set_status(name.capitalize() + " undone") + elif method == revelation.data.REDO: + self.statusbar.set_status(name.capitalize() + " redone") + + if len(iters) > 0: + self.tree.select(iters[0]) + else: + self.tree.unselect_all() + + + def undo_add_action(self, action, iters, extradata = None): + if not isinstance(iters, list) and iters != None: + iters = [iters] + + data = [] + name = { + self.entry_add : "Add Entry", + self.entry_remove : "Remove Entry", + self.entry_edit : "Edit Entry", + self.file_import : "File Import", + self.clip_cut : "Cut", + self.clip_paste : "Paste" + }[action] + + + for iter in iters: + + item = ({ + "path" : self.data.get_path(iter), + "parent" : self.data.get_path(self.data.iter_parent(iter)) + }) + + if action in [ self.entry_add, self.entry_remove, self.clip_cut, self.clip_paste, self.file_import ]: + item["data"] = self.data.export_entrystore([iter]) + + elif action == self.entry_edit: + item["data"] = self.data.get_entry(iter) + item["predata"] = extradata + + data.append(item) + + self.undoqueue.add_action(name, action, data) + + + +if __name__ == "__main__": + gnome.init(revelation.APPNAME, revelation.APPNAME) + + file = None + if len(sys.argv) > 1: + file = os.path.abspath(sys.argv[1]) + + app = Revelation() + app.run(file) +