#!/usr/bin/env python # -*- coding: utf-8 -* ''' Metadata anonymisation toolkit - GUI edition ''' import gtk import gobject import gettext import locale import logging import os import sys import mimetypes import xml.sax import urllib2 from lib import mat from lib import strippers logging.basicConfig(level=mat.LOGGING_LEVEL) class CFile(object): ''' Contain the "parser" class of the file "filename" This class exist just to be "around" my parser.Generic_parser class, since the gtk.ListStore does not accept it. ''' def __init__(self, filename, backup, add2archive): try: self.file = mat.create_class_file(filename, backup, add2archive) except: self.file = None class GUI: ''' Main GUI class ''' def __init__(self): # Preferences self.force = False self.backup = True self.add2archive = True # Main window self.window = gtk.Window() self.window.set_title('Metadata Anonymisation Toolkit') self.window.connect('destroy', gtk.main_quit) self.window.set_default_size(800, 600) path = os.path.join(mat.get_sharedir(), 'logo.png') icon = gtk.gdk.pixbuf_new_from_file_at_size(path, 50, 50) self.window.set_icon(icon) self.accelerator = gtk.AccelGroup() self.window.add_accel_group(self.accelerator) vbox = gtk.VBox() self.window.add(vbox) menubar = self.__create_menu() toolbar = self.__create_toolbar() content = gtk.ScrolledWindow() content.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) vbox.pack_start(menubar, False, True, 0) vbox.pack_start(toolbar, False, True, 0) vbox.pack_start(content, True, True, 0) # parser.class - name - path - type - cleaned self.liststore = gtk.ListStore(object, str, str, str, str, str) self.treeview = gtk.TreeView(model=self.liststore) self.treeview.set_search_column(1) # filename column is searchable self.treeview.set_rules_hint(True) # alternate colors for rows self.treeview.set_rubber_banding(True) # mouse selection self.treeview.connect("key_press_event", self.treeview_keyboard_event) self.treeview.connect('row-activated', self.__popup_metadata) self.treeview.connect('drag_data_received', self.__on_drag_data_received) self.treeview.drag_dest_set(gtk.DEST_DEFAULT_MOTION | gtk.DEST_DEFAULT_HIGHLIGHT | gtk.DEST_DEFAULT_DROP, [('text/uri-list', 0, 80), ], gtk.gdk.ACTION_COPY) self.__add_columns() self.selection = self.treeview.get_selection() self.selection.set_mode(gtk.SELECTION_MULTIPLE) content.add(self.treeview) self.statusbar = gtk.Statusbar() self.statusbar.push(1, _('Ready')) vbox.pack_start(self.statusbar, False, False, 0) self.window.show_all() def __create_toolbar(self): ''' Returns a vbox object, which contains a toolbar with buttons ''' toolbar = gtk.Toolbar() toolbutton = gtk.ToolButton(gtk.STOCK_ADD) toolbutton.set_label(_('Add')) toolbutton.connect('clicked', self.__add_files) toolbutton.set_tooltip_text(_('Add files')) toolbar.add(toolbutton) toolbutton = gtk.ToolButton(gtk.STOCK_CLEAR) toolbutton.set_label(_('Clean')) toolbutton.connect('clicked', self.__process_files, self.__mat_clean) toolbutton.set_tooltip_text(_('Clean selected files')) toolbar.add(toolbutton) toolbutton = gtk.ToolButton(gtk.STOCK_FIND) toolbutton.set_label(_('Check')) toolbutton.connect('clicked', self.__process_files, self.__mat_check) toolbutton.set_tooltip_text(_('Check selected files for harmful meta')) toolbar.add(toolbutton) toolbutton = gtk.ToolButton(stock_id=gtk.STOCK_QUIT) toolbutton.set_label(_('Quit')) toolbutton.connect('clicked', gtk.main_quit) toolbar.add(toolbutton) vbox = gtk.VBox(spacing=3) vbox.pack_start(toolbar, False, False, 0) return vbox def __add_columns(self): ''' Create the columns, and add them to the treeview ''' colname = [_('Path'), _('Filename'), _('Mimetype'), _('State'), _('Cleaned file')] for i, j in enumerate(colname, 1): filename_column = gtk.CellRendererText() column = gtk.TreeViewColumn(j, filename_column, text=i) column.set_sort_column_id(i) column.set_resizable(True) # column is resizeable self.treeview.append_column(column) def __create_menu_item(self, name, func, menu, pix, shortcut): ''' Create a MenuItem() like Preferences, Quit, Add, Clean, ... ''' item = gtk.ImageMenuItem() if shortcut: key, mod = gtk.accelerator_parse(shortcut) item.add_accelerator('activate', self.accelerator, key, mod, gtk.ACCEL_VISIBLE) picture = gtk.Image() picture.set_from_stock(pix, gtk.ICON_SIZE_MENU) item.set_image(picture) item.set_label('_' + name) item.set_use_underline(True) item.connect('activate', func) menu.append(item) def __create_sub_menu(self, name, menubar): ''' Create a submenu like File, Edit, Clean, ... ''' submenu = gtk.Menu() menuitem = gtk.MenuItem() menuitem.set_submenu(submenu) menuitem.set_label('_' + name) menuitem.set_use_underline(True) menubar.append(menuitem) return submenu def __create_menu(self): ''' Return a MenuBar ''' menubar = gtk.MenuBar() file_menu = self.__create_sub_menu(_('Files'), menubar) self.__create_menu_item(_('Add files'), self.__add_files, file_menu, gtk.STOCK_ADD, 'O') self.__create_menu_item(_('Quit'), gtk.main_quit, file_menu, gtk.STOCK_QUIT, 'Q') edit_menu = self.__create_sub_menu(_('Edit'), menubar) self.__create_menu_item(_('Clear the filelist'), lambda x: self.liststore.clear(), edit_menu, gtk.STOCK_REMOVE, None) self.__create_menu_item(_('Preferences'), self.__preferences, edit_menu, gtk.STOCK_PREFERENCES, 'P') process_menu = self.__create_sub_menu(_('Process'), menubar) item = gtk.ImageMenuItem() key, mod = gtk.accelerator_parse('L') item.add_accelerator('activate', self.accelerator, key, mod, gtk.ACCEL_VISIBLE) picture = gtk.Image() picture.set_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU) item.set_image(picture) item.set_label(_('Clean')) item.connect('activate', self.__process_files, self.__mat_clean) process_menu.append(item) item = gtk.ImageMenuItem() key, mod = gtk.accelerator_parse('h') item.add_accelerator('activate', self.accelerator, key, mod, gtk.ACCEL_VISIBLE) picture = gtk.Image() picture.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU) item.set_image(picture) item.set_label(_('Check')) item.connect('activate', self.__process_files, self.__mat_check) process_menu.append(item) help_menu = self.__create_sub_menu(_('Help'), menubar) self.__create_menu_item(_('Supported formats'), self.__supported, help_menu, gtk.STOCK_INFO, False) self.__create_menu_item(_('About'), self.__about, help_menu, gtk.STOCK_ABOUT, False) return menubar def treeview_keyboard_event(self, widget, event): ''' Remove selected files from the treeview when the use hit the 'suppr' key ''' if gtk.gdk.keyval_name(event.keyval) == "Delete": rows = [] self.selection.selected_foreach( lambda model, path, iter: rows.append(iter)) [self.liststore.remove(i) for i in rows] def __add_files(self, button): ''' Add the files chosed by the filechoser ("Add" button) ''' chooser = gtk.FileChooserDialog(title=_('Choose files'), parent=self.window, action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_OK, 0, gtk.STOCK_CANCEL, 1)) chooser.set_default_response(0) chooser.set_select_multiple(True) all_filter = gtk.FileFilter() # filter that shows all files all_filter.set_name(_('All files')) all_filter.add_pattern('*') chooser.add_filter(all_filter) supported_filter = gtk.FileFilter() # filter that shows only supported formats [supported_filter.add_mime_type(i) for i in strippers.STRIPPERS.keys()] supported_filter.set_name(_('Supported files')) chooser.add_filter(supported_filter) response = chooser.run() if response is 0: # gtk.STOCK_OK filenames = chooser.get_filenames() task = self.populate(filenames) gobject.idle_add(task.next) # asynchrone processing chooser.destroy() def populate(self, filenames): ''' Append selected files by add_file to the self.liststore ''' not_supported = [] for filename in filenames: # filenames : all selected files/folders if os.path.isdir(filename): # if "filename" is a directory for root, dirs, files in os.walk(filename): for item in files: path_to_file = os.path.join(root, item) if self.__add_file_to_treeview(path_to_file): not_supported.append(item) else: # filename is a regular file if self.__add_file_to_treeview(filename): not_supported.append(filename) yield True if not_supported: self.__popup_non_supported(not_supported) yield False def __add_file_to_treeview(self, filename): ''' Add a file to the list if it's format is supported ''' if not os.path.isfile(filename): # if filename does not exist return False cf = CFile(filename, self.backup, self.add2archive) if cf.file is not None: # if the file is supported by the mat self.liststore.append([cf, os.path.dirname(cf.file.filename) + os.path.sep, cf.file.basename, cf.file.mime, _('unknow'), 'None']) return False else: return True def __popup_metadata(self, widget, row, col): ''' Popup that display on double-clic metadata from a file ''' label = '%s\'s metadatas:\n' % self.liststore[row][1] meta = '' if self.liststore[row][4] == _('Clean') or\ self.liststore[row][0].file.is_clean(): meta = 'No metadata found' self.liststore[row][4] = _('Clean') else: self.liststore[row][4] = _('Dirty') iterator = self.liststore[row][0].file.get_meta().iteritems() for i, j in iterator: name = '-' + str(i) + ' : ' meta += (name + str(j) + '\n') w = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO, gtk.BUTTONS_CLOSE, label) w.set_resizable(True) w.set_size_request(400, 300) scrolled_window = gtk.ScrolledWindow() scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) w.vbox.pack_start(scrolled_window, True, True, 0) content = gtk.Label(meta) content.set_selectable(True) content.set_alignment(0, 0) content.set_use_markup(True) scrolled_window.add_with_viewport(content) w.set_markup(label) w.show_all() click = w.run() if click: w.destroy() def __popup_non_supported(self, filelist): ''' Popup that warn the user about the unsupported files that he want to process ''' dialog = gtk.Dialog(title=_('Not-supported'), parent=self.window, flags=0, buttons=(gtk.STOCK_OK, 0)) vbox = gtk.VBox(spacing=5) dialog.get_content_area().pack_start(vbox, True, True, 0) store = gtk.ListStore(str, str) # append filename - mimetype to the store #FIXME : I'm ugly for item in filelist: mime = mimetypes.guess_type(item)[0] if mime: store.append([item, mime]) else: store.append([item, 'unknown']) treeview = gtk.TreeView(store) vbox.pack_start(treeview, True, True, 0) #create column rendererText = gtk.CellRendererText() column = gtk.TreeViewColumn(_('Filename'), rendererText, text=0) treeview.append_column(column) column = gtk.TreeViewColumn(_('Mimetype'), rendererText, text=1) treeview.append_column(column) dialog.show_all() click = dialog.run() if click is 0: # Ok button dialog.destroy() def __about(self, button): ''' About popup ''' w = gtk.AboutDialog() w.set_authors(['Julien (jvoisin) Voisin', ]) w.set_artists(['Marine BenoƮt', ]) w.set_copyright('GNU Public License v2') w.set_comments(_('This software was coded during the GSoC 2011')) w.set_logo(gtk.gdk.pixbuf_new_from_file_at_size('logo.png', 400, 200)) w.set_program_name('Metadata Anonymisation Toolkit') w.set_version(mat.__version__) w.set_website('https://mat.boum.org') w.set_website_label(_('Website')) w.set_position(gtk.WIN_POS_CENTER) w.run() w.destroy() def __supported(self, button): ''' List the supported formats ''' dialog = gtk.Dialog(_('Supported formats'), self.window, 0, (gtk.STOCK_CLOSE, 0)) vbox = gtk.VBox(spacing=5) dialog.get_content_area().pack_start(vbox, True, True, 0) label = gtk.Label() label.set_markup('Supported fileformats') vbox.pack_start(label, True, True, 0) #parsing xml handler = mat.XMLParser() parser = xml.sax.make_parser() parser.setContentHandler(handler) path = os.path.join(mat.get_sharedir(), 'FORMATS') with open(path, 'r') as xmlfile: parser.parse(xmlfile) def expander_callback(current): ''' Close every expander except the current one ''' for i in vbox.get_children()[1:]: # first child is a gtk.Label if i != current: i.set_expanded(False) for item in handler.list: # list of dict : one dict per format # create one expander per format # only if the format is supported if item['mimetype'].split(',')[0] in strippers.STRIPPERS: # some format have more than one mimetype title = '%s (%s)' % (item['name'], item['extension']) support = ('\t%s : %s' % ('support', item['support'])) metadata = '\n\tmetadata : ' + item['metadata'] method = '\n\tmethod : ' + item['method'] content = support + metadata + method if item['support'] == 'partial': content += '\n\tremaining : ' + item['remaining'] expander = gtk.Expander(title) vbox.pack_start(expander, False, False, 0) label = gtk.Label() label.set_markup(content) expander.add(label) expander.connect('activate', expander_callback) dialog.show_all() click = dialog.run() if click is 0: # Close dialog.destroy() def __preferences(self, button): ''' Preferences popup ''' dialog = gtk.Dialog(_('Preferences'), self.window, 0, (gtk.STOCK_OK, 0)) hbox = gtk.HBox() dialog.get_content_area().pack_start(hbox, False, False, 0) icon = gtk.Image() icon.set_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_DIALOG) hbox.pack_start(icon, False, False, 20) table = gtk.Table(3, 2, False) # nb rows, nb lines hbox.pack_start(table, True, True, 0) force = gtk.CheckButton(_('Force Clean'), False) force.set_active(self.force) force.connect('toggled', self.__invert, 'force') force.set_tooltip_text(_('Do not check if already clean before \ cleaning')) table.attach(force, 0, 1, 0, 1) backup = gtk.CheckButton(_('Backup'), False) backup.set_active(self.backup) backup.connect('toggled', self.__invert, 'backup') backup.set_tooltip_text(_('Keep a backup copy')) table.attach(backup, 0, 1, 1, 2) add2archive = gtk.CheckButton(_('Add unsupported file to archives'), False) add2archive.set_active(self.add2archive) add2archive.connect('toggled', self.__invert, 'add2archive') add2archive.set_tooltip_text(_('Add non-supported (and so \ non-anonymised) file to output archive')) table.attach(add2archive, 0, 1, 2, 3) hbox.show_all() response = dialog.run() if response is 0: # gtk.STOCK_OK dialog.destroy() def __invert(self, button, name): ''' Invert a preference state ''' if name == 'force': self.force = not self.force elif name == 'backup': self.backup = not self.backup for line in xrange(len(self.liststore)): # change the "backup" property of all files self.liststore[line][0].file.backup = self.backup self.treeview.get_column(4).set_visible(self.backup) elif name == 'add2archive': self.add2archive = not self.add2archive def __on_drag_data_received(self, widget, context, x, y, selection, target_type, timestamp): ''' This function is called when something is drag'n'droped into mat. It basically add files. ''' urls = selection.data.strip('\r\n\x00') # strip stupid characters cleaned_urls = map(self.__clean_draged_file_path, urls.split('\n')) task = self.populate(cleaned_urls) gobject.idle_add(task.next) # asynchrone processing def __clean_draged_file_path(self, url): ''' Since the dragged urls are ugly, we need to process them ''' url = urllib2.unquote(url) # unescape stupid chars if url.startswith('file:\\\\\\'): # windows return url[8:] # 8 is len('file:///') elif url.startswith('file://'): # nautilus, rox return url[7:] # 7 is len('file://') elif url.startswith('file:'): # xffm return url[5:] # 5 is len('file:') def __process_files(self, button, func): ''' Launch the function "func" in a asynchrone way ''' iterator = self.selection.get_selected_rows()[1] if not iterator: # if nothing is selected : select everything iterator = xrange(len(self.liststore)) task = func(iterator) # launch func() in an asynchrone way gobject.idle_add(task.next) def __mat_check(self, iterator): ''' Check if selected elements are clean ''' for line in iterator: # for each file in selection self.statusbar.push(0, _('Checking %s...') % self.liststore[line][1]) if self.force is True or self.liststore[line][4] != _('Clean'): if self.liststore[line][0].file.is_clean(): string = _('Clean') else: string = _('Dirty') logging.info('%s is %s' % (self.liststore[line][1], string)) self.liststore[line][4] = string yield True self.statusbar.push(0, _('Ready')) yield False def __mat_clean(self, iterator): ''' Clean selected elements ''' for line in iterator: # for each file in selection logging.info('Cleaning %s' % self.liststore[line][1]) self.statusbar.push(0, _('Cleaning %s...') % self.liststore[line][1]) if self.force is True or self.liststore[line][4] != _('Clean'): if self.liststore[line][0].file.remove_all(): self.liststore[line][4] = _('Clean') if self.backup: # the backup copy state self.liststore[line][5] = os.path.basename(self.liststore[line][0].file.output) yield True self.statusbar.push(0, _('Ready')) yield False def translate(): '''' Handle L10N of mat-gui ''' # get system's language current_lang, _ = locale.getdefaultlocale() if current_lang: langs = [current_lang] # get the $LANG environnement's variable language = os.environ.get('LANG', None) if language: langs += language.split(':') return gettext.translation('mat-gui', 'locale', langs, fallback=True).ugettext if __name__ == '__main__': _ = translate() #Main gui = GUI() #Add files from command line infiles = [arg for arg in sys.argv[1:] if os.path.exists(arg)] if infiles: task = gui.populate(infiles) gobject.idle_add(task.next) gtk.main()