#!/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 <b>include</b>'
' 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()