#!/usr/bin/env python # -*- coding: utf-8 -* """ Metadata anonymisation toolkit - GUI edition """ import gi gi.require_version('Gtk', '3.0') from gi.repository import GObject, Gtk, GLib from gi.repository import Gdk, GdkPixbuf import gettext import logging import os import sys import xml.sax try: from urllib2 import unquote except ImportError: # python3 from urllib.parse import unquote from libmat import mat from libmat import strippers from libmat import parser from libmat import archive logging.basicConfig(level=mat.LOGGING_LEVEL) class CFile(GObject.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 because it does not extends Gobject.Object """ def __init__(self, filename, **kwargs): self.file = mat.create_class_file(filename, False, **kwargs) class GUI(object): """ Main GUI class """ def __init__(self): # Preferences self.add2archive = False self.pdf_quality = False # Main window self.builder = Gtk.Builder() self.builder.set_translation_domain('MAT') self.builder.add_from_file(mat.get_datafile_path('mat.glade')) self.builder.connect_signals(self) self.logo = mat.get_logo() icon = GdkPixbuf.Pixbuf.new_from_file_at_size(self.logo, 50, 50) self.window = self.builder.get_object('MainWindow') self.window.set_icon(icon) self.liststore = self.builder.get_object('MainWindowListstore') ''' The Liststore contains: 0: The CFile instance that represents the file 1: The file's name 2: The file's state (Localised string) ''' self.treeview = self.builder.get_object('MainWindowTreeview') self.statusbar = self.builder.get_object('Statusbar') self.statusbar.push(1, _('Ready')) self.__init_supported_popup() self.__set_drag_treeview() self.window.show_all() def __init_supported_popup(self): """ Initialise the "supported formats" popup """ self.supported_dict = mat.XMLParser() xml_parser = xml.sax.make_parser() xml_parser.setContentHandler(self.supported_dict) path = mat.get_datafile_path('FORMATS') with open(path, 'r') as xmlfile: xml_parser.parse(xmlfile) supported_cbox = self.builder.get_object('supported_cbox') store = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING) for i, j in enumerate(self.supported_dict.list): store.append([i, j['name']]) supported_cbox.set_model(store) supported_cbox.set_entry_text_column(1) supported_cbox.set_active(0) self.builder.get_object('supported_metadata').set_buffer(Gtk.TextBuffer()) self.builder.get_object('supported_remaining').set_buffer(Gtk.TextBuffer()) self.builder.get_object('supported_method').set_buffer(Gtk.TextBuffer()) self.cb_update_supported_popup(supported_cbox) # to initially fill the dialog def __set_drag_treeview(self): """ Setup the drag'n'drop handling by the treeview """ self.treeview.drag_dest_set( Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT | Gtk.DestDefaults.DROP, [], Gdk.DragAction.COPY) targets = Gtk.TargetList.new([]) targets.add_uri_targets(80) self.treeview.drag_dest_set_target_list(targets) @staticmethod def cb_hide_widget(widget, _): """ This function is a little hack to hide instead of close re-usable popups, like supported-fileformats, popup-metadata, ...""" widget.hide() return False def cb_update_supported_popup(self, window): """ Fill GtkEntries of the supported_format_popups with corresponding data. """ index = window.get_model()[window.get_active_iter()][0] support = self.builder.get_object('supported_support') support.set_text(self.supported_dict.list[index]['support']) metadata = self.builder.get_object('supported_metadata').get_buffer() metadata.set_text(self.supported_dict.list[index]['metadata']) method = self.builder.get_object('supported_method').get_buffer() method.set_text(self.supported_dict.list[index]['method']) remaining = self.builder.get_object('supported_remaining').get_buffer() remaining.set_text(self.supported_dict.list[index]['remaining']) @staticmethod def cb_close_application(_): """ Close the application """ Gtk.main_quit() def cb_add_files(self, button): """ Add the files chosen by the filechooser ("Add" button) """ chooser = Gtk.FileChooserDialog(title=_('Choose files'), parent=self.window, action=Gtk.FileChooserAction.OPEN, buttons=(Gtk.STOCK_OK, 0, Gtk.STOCK_CANCEL, 1)) chooser.set_default_response(0) chooser.set_select_multiple(True) # filter that shows only supported formats supported_filter = Gtk.FileFilter() supported_filter.set_name(_('Supported files')) for i in strippers.STRIPPERS.keys(): supported_filter.add_mime_type(i) chooser.add_filter(supported_filter) # filter that shows all files all_filter = Gtk.FileFilter() all_filter.set_name(_('All files')) all_filter.add_pattern('*') chooser.add_filter(all_filter) if not chooser.run(): # Gtk.STOCK_OK filenames = chooser.get_filenames() GLib.idle_add(self.populate(filenames).next) # asynchronous processing chooser.destroy() def cb_popup_metadata(self, widget, row, col): """ Popup that display on double-click metadata from a file """ metadataPopupListStore = self.builder.get_object('MetadataPopupListStore') metadataPopupListStore.clear() if self.liststore[row][0].file.is_clean(): self.liststore[row][2] = _('Clean') metadataPopupListStore.append([_('No metadata found'), '']) else: self.liststore[row][2] = _('Dirty') for i, j in self.liststore[row][0].file.get_meta().items(): metadataPopupListStore.append([i, j]) popup_metadata = self.builder.get_object('MetadataPopup') title = self.liststore[row][0].file.basename popup_metadata.set_title(_("%s's metadata") % title.decode('utf8')) popup_metadata.show_all() popup_metadata.run() popup_metadata.hide() def cb_about_popup(self, button): """ About popup """ w = Gtk.AboutDialog() w.set_authors(['Julien (jvoisin) Voisin', ]) w.set_artists(['Marine BenoƮt', ]) w.set_copyright('GNU General Public License v2') w.set_comments(_('Trash your meta, keep your data')) w.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_size(self.logo, 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.WindowPosition.CENTER) w.set_transient_for(self.window) w.run() w.destroy() def cb_supported_popup(self, w): """ Show the "supported formats" popup""" dialog = self.builder.get_object('SupportedWindow') dialog.show_all() dialog.run() dialog.hide() def cb_clear_list(self, _): """ Clear the file list """ self.liststore.clear() def cb_mat_check(self, button): """ Callback for checking files """ self.__process_files(self.__mat_check) def cb_mat_clean(self, button): """ Callback for cleaning files """ self.__process_files(self.__mat_clean) def cb_preferences_popup(self, button): """ Preferences popup """ dialog = Gtk.Dialog(_('Preferences'), self.window, 0, (Gtk.STOCK_OK, 0)) dialog.connect('delete-event', self.cb_hide_widget) dialog.set_resizable(False) hbox = Gtk.HBox() dialog.get_content_area().pack_start(hbox, False, False, 0) icon = Gtk.Image() icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.DIALOG) hbox.pack_start(icon, False, False, 20) table = Gtk.Table(2, 2, False) # nb rows, nb lines hbox.pack_start(table, True, True, 0) pdf_quality = Gtk.CheckButton(_('Reduce PDF quality'), False) pdf_quality.set_active(self.pdf_quality) pdf_quality.connect('toggled', self.__invert, 'pdf_quality') pdf_quality.set_tooltip_text(_('Reduce the produced PDF size and quality')) table.attach(pdf_quality, 0, 1, 0, 1) add2archive = Gtk.CheckButton(_('Remove unsupported file from archives'), False) add2archive.set_active(not self.add2archive) add2archive.connect('toggled', self.__invert, 'add2archive') add2archive.set_tooltip_text(_('Remove non-supported (and so \ non-anonymised) file from output archive')) table.attach(add2archive, 0, 1, 1, 2) hbox.show_all() if not dialog.run(): # Gtk.STOCK_OK for f in self.liststore: # update preferences f[0].file.add2archive = self.add2archive if f[0].file.mime.startswith('pdf'): f[0].file.pdf_quality = self.pdf_quality dialog.hide() def cb_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. """ def clean_path(url): """ Since the dragged urls are ugly, we need to process them """ url = unquote(url) # unquote url url = url.decode('utf-8') # decode in utf-8 if url.startswith('file:\\\\\\'): # windows return url[8:] # 8 is len('file:///') elif url.startswith('file://'): # nautilus, rox, thunar return url[7:] # 7 is len('file://') elif url.startswith('file:'): # xffm return url[5:] # 5 is len('file:') dirty_urls = selection.get_uris() cleaned_urls = map(clean_path, dirty_urls) GLib.idle_add(self.populate(cleaned_urls).next) # asynchronous processing def __add_file_to_treeview(self, filename): """ Add a file to the list if its format is supported """ cf = CFile(filename, add2archive=self.add2archive, low_pdf_quality=self.pdf_quality) if cf.file and cf.file.is_writable: self.liststore.append([cf, cf.file.basename, _('Unknown')]) return False return True def __process_files(self, func): """ Launch the function "func" in a asynchronous way """ iterator = self.treeview.get_selection().get_selected_rows()[1] if not iterator: # if nothing is selected : select everything iterator = range(len(self.liststore)) sync_task = func(iterator) # launch func() in an asynchronous way GLib.idle_add(sync_task.next) def __invert(self, button, name): """ Invert a preference state """ if name == 'pdf_quality': self.pdf_quality = not self.pdf_quality elif name == 'add2archive': self.add2archive = not self.add2archive def populate(self, filenames): """ Append selected files by add_file to the self.liststore :param filenames: selected files """ 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) yield True else: # filename is a regular file if self.__add_file_to_treeview(filename): not_supported.append(filename) yield True self.cb_mat_check(None) if not_supported: self.__popup_non_supported(not_supported) yield False 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=Gtk.DialogFlags.MODAL, buttons=(Gtk.STOCK_OK, 0)) dialog.set_size_request(220, 180) vbox = Gtk.VBox(spacing=5) sc = Gtk.ScrolledWindow() sc.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sc.add_with_viewport(vbox) dialog.get_content_area().pack_start(sc, True, True, 0) store = Gtk.ListStore(str, str) # appends "filename - reason" to the ListStore for item in filelist: if os.path.splitext(item)[1] in parser.NOMETA: store.append([os.path.basename(item), _('Harmless fileformat')]) elif not os.access(item, os.R_OK): store.append([os.path.basename(item), _('Cant read file')]) else: store.append([os.path.basename(item), _('Fileformat not supported')]) treeview = Gtk.TreeView(store) vbox.pack_start(Gtk.Label(_('These files can not be processed:')), False, False, 0) vbox.pack_start(treeview, True, True, 0) # Create columns rendererText = Gtk.CellRendererText() column = Gtk.TreeViewColumn(_('Filename'), rendererText, text=0) treeview.append_column(column) column = Gtk.TreeViewColumn(_('Reason'), rendererText, text=1) treeview.append_column(column) dialog.show_all() dialog.run() dialog.destroy() def __popup_archive(self, file_name, files_list): """ Popup that shows the user what files are not going to be include into to outputed archive """ dialog = Gtk.Dialog(title=_('Non-supported files in archive'), parent=self.window, flags=Gtk.DialogFlags.MODAL, buttons=(_('Clean'), 0)) dialog.set_size_request(220, 180) vbox = Gtk.VBox(spacing=5) sc = Gtk.ScrolledWindow() sc.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sc.add_with_viewport(vbox) dialog.get_content_area().pack_start(sc, True, True, 0) store = Gtk.ListStore(GObject.TYPE_BOOLEAN, GObject.TYPE_STRING) for i in files_list: # store.extend is not supported, wtf?! store.append([0, os.path.basename(i)]) treeview = Gtk.TreeView(store) column_toggle = Gtk.TreeViewColumn(_('Include')) column_text = Gtk.TreeViewColumn(_('Filename')) treeview.append_column(column_toggle) treeview.append_column(column_text) cellrenderer_text = Gtk.CellRendererText() column_text.pack_start(cellrenderer_text, False) column_text.add_attribute(cellrenderer_text, 'text', 1) cellrenderer_toggle = Gtk.CellRendererToggle() column_toggle.pack_start(cellrenderer_toggle, True) column_toggle.add_attribute(cellrenderer_toggle, 'active', 0) def cell_toggled(widget, path, model): model[path][0] = not model[path][0] cellrenderer_toggle.connect('toggled', cell_toggled, store) vbox.pack_start(Gtk.Label(_('MAT is not able to clean the' ' following files, found in the %s archive') % file_name), False, False, 0) label = Gtk.Label() label.set_markup('Select the files you want to include' ' in the cleaned up archive anyway.') vbox.pack_start(label, False, False, 0) vbox.pack_start(treeview, True, True, 0) dialog.show_all() dialog.run() dialog.destroy() return [i[1] for i in store if i[0]] def __mat_check(self, iterator): """ Check elements in iterator are clean """ for line in iterator: # for each file in selection msg = _('Checking %s') % self.liststore[line][1].decode('utf-8', 'replace') logging.info(msg) self.statusbar.push(0, msg) if self.liststore[line][0].file.is_clean(): self.liststore[line][2] = _('Clean') else: self.liststore[line][2] = _('Dirty') logging.info('%s is %s' % (self.liststore[line][1], self.liststore[line][2])) yield True self.statusbar.push(0, _('Ready')) yield False def __mat_clean(self, iterator): """ Clean elements in iterator """ for line in iterator: # for each file in selection msg = _('Cleaning %s') % self.liststore[line][1].decode('utf-8', 'replace') logging.info(msg) self.statusbar.push(0, msg) is_archive = isinstance(self.liststore[line][0].file, archive.GenericArchiveStripper) is_terminal = isinstance(self.liststore[line][0].file, archive.TerminalZipStripper) list_to_add = [] if is_archive and not is_terminal: unsupported_list = self.liststore[line][0].file.list_unsupported() if type(unsupported_list) == list and unsupported_list: logging.debug("Unsupported list: %s" % unsupported_list) filename = os.path.basename(self.liststore[line][0].file.filename) list_to_add = self.__popup_archive(filename, unsupported_list) if self.liststore[line][0].file.remove_all(whitelist=list_to_add): self.liststore[line][2] = _('Clean') elif self.liststore[line][0].file.remove_all(): self.liststore[line][2] = _('Clean') yield True self.statusbar.push(0, _('Ready')) yield False if __name__ == '__main__': gettext.install('MAT', unicode=True) 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) GLib.idle_add(task.next) Gtk.main()