Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
587 lines (510 sloc) 25.6 KB
#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
#==============================================================================
"""
ExeFilter GUI
A Graphical User Interface for ExeFilter, using wxPython.
This file is part of the ExeFilter project:
U{http://www.decalage.info/exefilter}
@author: U{Philippe Lagadec<mailto:philippe.lagadec(a)laposte.net>}
@copyright: Philippe Lagadec 2010-2011
@license: CeCILL (open-source compatible GPL)
see file LICENCE.txt for the full license
@version: 0.03
@status: alpha
"""
#==============================================================================
__docformat__ = 'epytext en'
__author__ = "Philippe Lagadec"
__date__ = "2011-04-30"
__version__ = "0.03"
#------------------------------------------------------------------------------
# LICENCE pour le projet ExeFilter:
# Copyright DGA/CELAR 2004-2008
# Copyright NATO/NC3A 2008-2010 (modifications PL apres ExeFilter v1.1.0)
# Copyright Philippe Lagadec 2010-2011
# Auteurs:
# - Philippe Lagadec (PL) - philippe.lagadec(a)laposte.net
# - Arnaud Kerréneur (AK) - arnaud.kerreneur(a)dga.defense.gouv.fr
# - Tanguy Vinceleux (TV) - tanguy.vinceleux(a)dga.defense.gouv.fr
#
# Ce logiciel est régi par la licence CeCILL soumise au droit français et
# respectant les principes de diffusion des logiciels libres. Vous pouvez
# utiliser, modifier et/ou redistribuer ce programme sous les conditions
# de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA
# sur le site "http://www.cecill.info". Une copie de cette licence est jointe
# dans les fichiers Licence_CeCILL_V2-fr.html et Licence_CeCILL_V2-en.html.
#
# En contrepartie de l'accessibilité au code source et des droits de copie,
# de modification et de redistribution accordés par cette licence, il n'est
# offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons,
# seule une responsabilité restreinte pèse sur l'auteur du programme, le
# titulaire des droits patrimoniaux et les concédants successifs.
#
# A cet égard l'attention de l'utilisateur est attirée sur les risques
# associés au chargement, à l'utilisation, à la modification et/ou au
# développement et à la reproduction du logiciel par l'utilisateur étant
# donné sa spécificité de logiciel libre, qui peut le rendre complexe à
# manipuler et qui le réserve donc à des développeurs et des professionnels
# avertis possédant des connaissances informatiques approfondies. Les
# utilisateurs sont donc invités à charger et tester l'adéquation du
# logiciel à leurs besoins dans des conditions permettant d'assurer la
# sécurité de leurs systèmes et ou de leurs données et, plus généralement,
# à l'utiliser et l'exploiter dans les mêmes conditions de sécurité.
#
# Le fait que vous puissiez accéder à cet en-tête signifie que vous avez
# pris connaissance de la licence CeCILL, et que vous en avez accepté les
# termes.
#------------------------------------------------------------------------------
# CHANGELOG:
# generated by wxGlade 0.6.3 on Wed Dec 29 07:28:18 2010
# 2011-01-24 v0.01 PL: - first working version
# 2011-02-03 v0.02 PL: - fixed main bugs, minimal functionality
# 2011-04-30 v0.03 PL: - added scan mode launched by scan button
#--- TODO ---------------------------------------------------------------------
# + file/load policy, save report as HTML/XML
# - catch and display exceptions
# - test with FR locale
# - test with accents
# - optimize similar code in scan and clean functions
#LATER:
# + portable way to open doc
# + help/link to websites
# + command line options to directly scan or clean a file then show results in
# GUI
# + CLI option to open the cleaned file afterwards, with confirmation popup if
# cleaned, blocked or error
# + use status bar or remove it?
# + set all tooltips
# - option to show logs? (and then hide console)
# + py2exe version, with ruby2exe for origami:
# http://www.decalage.info/en/python/py2exe
# http://www.erikveen.dds.nl/rubyscript2exe/
# + list: use icons instead of background color?
# + list: white instead of green?
# + error msg box if wx can't be imported or if an error happens in the main app
# loop, using easygui or tkinter
# - progress dialog with % => use a separate thread for transfert
# - populate list during scan
# + list: autosize, but according to window
# + list: possible to wrap long lines? => use ultimatelistctrl from wx.lib.agw
# see http://xoomer.virgilio.it/infinity77/AGW_Docs/index.html
# BUT check first if it works on Linux/Mac!
# + list: fix resultat to have shorter columns:
# - recognized type(s)
# - type according to extension
# - type according to header
# - active content
# - show paths as relative to home dir?
# - default destination path in home dir/exefilter_output?
# - button to open dest dir
# - double-click on a file to open it (with confirmation)
# - builtin text/hex viewer?
# - show type from ext, magic, md5/sha1 (optional)
# - link to policy editor
# - combobox+button to choose/load policy (or just menu?)
# - save combobox values
# - alternate view with a treectrl: using colors and icons to show the tree
# structure. selecting a node shows more details on the right.
# OR could also use wx.gizmos.TreeListCtrl to have both tree and list
# (see c:\Program Files\wxPython2.8 Docs and Demos\demo\TreeListCtrl.py)
# - use specific icons for each filetype, with a (background?) color depending
# on the filter results. (issue when several filters apply)
#=== IMPORTS ==================================================================
import sys, os, os.path, traceback
import wx
import wx.grid
import ExeFilter as xf
import Rapport, Resultat, Journal
#=== CONSTANTS ================================================================
COLOR_CLEAN = wx.Colour(red=200, green=255, blue=200) # light green
COLOR_CLEANED = wx.Colour(red=255, green=255, blue=175) # light yellow
COLOR_ERROR = wx.Colour(red=255, green=240, blue=175) # light orange
COLOR_BLOCKED = wx.Colour(red=255, green=175, blue=175) # light red
#=== INIT =====================================================================
# init console logging (after setting the debug mode if -v option):
Journal.init_console_logging()
# begin wxGlade: extracode
# end wxGlade
#=== CLASSES ==================================================================
class MainFrame(wx.Frame):
def __init__(self, *args, **kwds):
# begin wxGlade: MainFrame.__init__
kwds["style"] = wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self, *args, **kwds)
# Menu Bar
self.main_frame_menubar = wx.MenuBar()
global ID_DOC; ID_DOC = wx.NewId()
wxglade_tmp_menu = wx.Menu()
wxglade_tmp_menu.Append(wx.ID_EXIT, "Exit", "", wx.ITEM_NORMAL)
self.main_frame_menubar.Append(wxglade_tmp_menu, "File")
wxglade_tmp_menu = wx.Menu()
wxglade_tmp_menu.Append(wx.ID_ABOUT, "About", "", wx.ITEM_NORMAL)
wxglade_tmp_menu.Append(ID_DOC, "Documentation", "", wx.ITEM_NORMAL)
self.main_frame_menubar.Append(wxglade_tmp_menu, "Help")
self.SetMenuBar(self.main_frame_menubar)
# Menu Bar end
self.label_source = wx.StaticText(self, -1, "Source:")
self.combo_box_source = wx.ComboBox(self, -1, choices=["demo_files"], style=wx.CB_DROPDOWN)
self.button_source_file = wx.Button(self, -1, "Select File...")
self.button_source_folder = wx.Button(self, -1, "Select Folder...")
self.label_dest = wx.StaticText(self, -1, "Destination:")
self.combo_box_dest = wx.ComboBox(self, -1, choices=["demo_output"], style=wx.CB_DROPDOWN)
self.button_dest_file = wx.Button(self, -1, "Select File...")
self.button_dest_folder = wx.Button(self, -1, "Select Folder...")
self.button_scan = wx.Button(self, -1, "Scan")
self.button_clean = wx.Button(self, -1, "Scan and Clean")
self.grid_results = wx.grid.Grid(self, -1, size=(1, 1))
self.list_results = wx.ListCtrl(self, -1, style=wx.LC_REPORT|wx.LC_HRULES|wx.LC_VRULES|wx.SUNKEN_BORDER)
self.__set_properties()
self.__do_layout()
self.Bind(wx.EVT_MENU, self.quit, id=wx.ID_EXIT)
self.Bind(wx.EVT_MENU, self.about, id=wx.ID_ABOUT)
self.Bind(wx.EVT_MENU, self.display_doc, id=ID_DOC)
self.Bind(wx.EVT_BUTTON, self.select_source_file, self.button_source_file)
self.Bind(wx.EVT_BUTTON, self.select_source_dir, self.button_source_folder)
self.Bind(wx.EVT_BUTTON, self.select_dest_file, self.button_dest_file)
self.Bind(wx.EVT_BUTTON, self.select_dest_dir, self.button_dest_folder)
self.Bind(wx.EVT_BUTTON, self.scan, self.button_scan)
self.Bind(wx.EVT_BUTTON, self.clean, self.button_clean)
# end wxGlade
# disabled grid
## self.grid_results = wx.grid.Grid(self, -1, size=(1, 1))
# set list columns
self.list_results.InsertColumn(col=0, heading='File')
self.list_results.InsertColumn(col=1, heading='Result')
self.list_results.InsertColumn(col=2, heading='Details')
## self.clear_grid()
def __set_properties(self):
# begin wxGlade: MainFrame.__set_properties
self.SetTitle("ExeFilter")
self.SetSize((600, 501))
self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DFACE))
self.combo_box_source.SetSelection(0)
self.combo_box_dest.SetSelection(0)
self.button_scan.SetToolTipString("Analyze the source file or folder, without cleaning")
self.button_clean.SetToolTipString("Analyze the source file or folder, copy a sanitized version to the destination folder")
self.grid_results.CreateGrid(10, 3)
self.grid_results.SetRowLabelSize(0)
self.grid_results.EnableEditing(0)
self.grid_results.EnableDragRowSize(0)
self.grid_results.EnableDragGridSize(0)
self.grid_results.SetSelectionMode(wx.grid.Grid.wxGridSelectRows)
self.grid_results.SetColLabelValue(0, "File")
self.grid_results.SetColLabelValue(1, "Result")
self.grid_results.SetColLabelValue(2, "Details")
self.grid_results.Enable(False)
self.grid_results.Hide()
# end wxGlade
# disabled grid
## self.grid_results.CreateGrid(10, 3)
## self.grid_results.SetRowLabelSize(0)
## self.grid_results.EnableEditing(0)
## self.grid_results.EnableDragRowSize(0)
## self.grid_results.EnableDragGridSize(0)
## self.grid_results.SetSelectionMode(wx.grid.Grid.wxGridSelectRows)
## self.grid_results.SetColLabelValue(0, "File")
## self.grid_results.SetColLabelValue(1, "Result")
## self.grid_results.SetColLabelValue(2, "Details")
def __do_layout(self):
# begin wxGlade: MainFrame.__do_layout
sizer_vertical = wx.BoxSizer(wx.VERTICAL)
sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
grid_sizer_2 = wx.FlexGridSizer(2, 4, 0, 0)
grid_sizer_2.Add(self.label_source, 0, wx.ALL|wx.EXPAND|wx.ALIGN_CENTER_VERTICAL, 2)
grid_sizer_2.Add(self.combo_box_source, 1, wx.EXPAND, 0)
grid_sizer_2.Add(self.button_source_file, 0, 0, 0)
grid_sizer_2.Add(self.button_source_folder, 0, 0, 0)
grid_sizer_2.Add(self.label_dest, 0, wx.EXPAND|wx.ALIGN_CENTER_VERTICAL, 2)
grid_sizer_2.Add(self.combo_box_dest, 1, wx.EXPAND, 0)
grid_sizer_2.Add(self.button_dest_file, 0, 0, 0)
grid_sizer_2.Add(self.button_dest_folder, 0, 0, 0)
grid_sizer_2.AddGrowableCol(1)
sizer_vertical.Add(grid_sizer_2, 0, wx.ALL|wx.EXPAND, 4)
sizer_3.Add(self.button_scan, 0, wx.ALIGN_CENTER_HORIZONTAL, 0)
sizer_3.Add((20, 20), 0, 0, 0)
sizer_3.Add(self.button_clean, 0, 0, 0)
sizer_vertical.Add(sizer_3, 0, wx.ALL|wx.ALIGN_CENTER_HORIZONTAL, 4)
sizer_vertical.Add(self.grid_results, 0, wx.EXPAND, 0)
sizer_vertical.Add(self.list_results, 1, wx.EXPAND, 0)
self.SetSizer(sizer_vertical)
self.Layout()
# end wxGlade
# disabled grid
## sizer_vertical.Add(self.grid_results, 1, wx.EXPAND, 0)
## def clear_grid(self):
## """
## clear the grid data
## """
## # delete all rows:
## n = self.grid_results.GetNumberRows()
## if n:
## self.grid_results.DeleteRows(0,n)
def select_source_file(self, event): # wxGlade: MainFrame.<event_handler>
prev_source = os.path.abspath(self.get_source())
if os.path.exists(prev_source) and os.path.isdir(prev_source):
# previous source was a dir, use it as starting point:
prev_path = prev_source
prev_source = ''
else:
# previous source was a file or does not exist, use its dir as starting point:
prev_path = os.path.dirname(prev_source)
source = wx.FileSelector(message='Select source file to be scanned',
default_path=prev_path, default_filename=prev_source,
flags=wx.OPEN|wx.FILE_MUST_EXIST)
if source:
self.set_source(source)
def select_source_dir(self, event): # wxGlade: MainFrame.<event_handler>
prev_source = os.path.abspath(self.get_source())
if os.path.exists(prev_source) and os.path.isfile(prev_source):
# previous source was a file, use its dir as starting point:
prev_source = os.path.dirname(prev_source)
source_dir = wx.DirSelector(message='Select source folder to be scanned',
defaultPath=prev_source)
if source_dir:
self.set_source(source_dir)
def select_dest_file(self, event): # wxGlade: MainFrame.<event_handler>
prev_dest = os.path.abspath(self.get_dest())
if os.path.exists(prev_dest) and os.path.isdir(prev_dest):
# previous dest was a dir, use it as starting point:
prev_path = prev_dest
# filename = same as source
prev_dest = os.path.basename(self.get_source())
else:
# previous dest was a file or does not exist, use its dir as starting point:
prev_path = os.path.dirname(prev_dest)
dest = wx.FileSelector(message='Enter the name of the destination file',
default_path=prev_path, default_filename=prev_dest,
flags=wx.SAVE|wx.OVERWRITE_PROMPT)
if dest:
self.set_dest(dest)
## fileDialog = wx.FileDialog(self, message='Enter the name of the destination file',
## defaultDir=self.get_dest(), defaultFile='', style=wx.SAVE|wx.OVERWRITE_PROMPT)
## result = fileDialog.ShowModal()
## if result == wx.ID_OK:
## #TODO: convert to relative path?
## self.dest_file = fileDialog.GetPath()
## #wx.LogMessage('You selected: %s\n' % self.source_file)
## self.set_dest(self.dest_file)
## fileDialog.Destroy()
def select_dest_dir(self, event): # wxGlade: MainFrame.<event_handler>
prev_dest = os.path.abspath(self.get_dest())
if os.path.exists(prev_dest) and os.path.isfile(prev_dest):
# previous dest was a file, use its dir as starting point:
prev_dest = os.path.dirname(prev_dest)
dest_dir = wx.DirSelector(message='Select destination folder for cleaned files',
defaultPath=prev_dest)
if dest_dir:
self.set_dest(dest_dir)
def get_source(self):
return self.combo_box_source.GetValue()
def get_dest(self):
return self.combo_box_dest.GetValue()
def set_source(self, source_path):
self.combo_box_source.SetValue(source_path)
def set_dest(self, dest_path):
self.combo_box_dest.SetValue(dest_path)
def clear_results(self):
"""
clear the list of results
"""
# delete all rows:
self.list_results.DeleteAllItems()
def scan(self, event): # wxGlade: MainFrame.<event_handler>
source = self.get_source()
if not os.path.exists(source):
wx.MessageBox('Error: the source path does not exist.', 'Error', wx.ICON_ERROR)
return
# delete all rows:
self.clear_results()
d = wx.ProgressDialog(title='Scanning', message='Scanning %s...' % source)
d.Pulse()
try:
if os.path.isdir(source):
wx.BeginBusyCursor()
xf.scan_dir(source)
wx.EndBusyCursor()
else:
wx.BeginBusyCursor()
xf.scan_file(source)
wx.EndBusyCursor()
#xf.display_html_report()
except:
wx.EndBusyCursor()
msg = 'Unhandled error while cleaning: please report it to decalage(a)laposte.net\n' + traceback.format_exc()
wx.MessageBox(msg, 'Error', wx.ICON_ERROR)
d.Update(value=100)
# it's necessary to destroy the dialog else the console stays open at the end
d.Destroy()
l = self.list_results
for result in Rapport.liste_resultats:
filename = result.chemin_fichier
index = l.InsertStringItem(sys.maxint, filename)
if result.code_resultat == Resultat.ACCEPTE:
text = 'CLEAN'
color = COLOR_CLEAN #wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN
elif result.code_resultat == Resultat.NETTOYE:
text = 'TO BE CLEANED'
color = COLOR_CLEANED #wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW
elif result.code_resultat == Resultat.REFUSE \
or result.code_resultat == Resultat.EXT_NON_AUTORISEE \
or result.code_resultat == Resultat.FORMAT_INCORRECT \
or result.code_resultat == Resultat.VIRUS :
text = 'NOT ALLOWED'
color = COLOR_BLOCKED #wx.TheColourDatabase.FindColour('RED') #wx.RED
elif result.code_resultat == Resultat.ERREUR_LECTURE \
or result.code_resultat == Resultat.ERREUR_ANALYSE:
text = 'ERROR'
color = COLOR_ERROR #wx.TheColourDatabase.FindColour('ORANGE')
l.SetStringItem(index, 1, text)
if result.code_resultat == Resultat.EXT_NON_AUTORISEE:
details = result.details() + '\n'
else:
details = ''
details += '\n'.join(result.raison)
l.SetStringItem(index, 2, details)
l.SetItemBackgroundColour(index, color)
for col in range(3):
l.SetColumnWidth(col=col, width=-1)
def clean(self, event): # wxGlade: MainFrame.<event_handler>
source = self.get_source()
dest = self.get_dest()
if not os.path.exists(source):
wx.MessageBox('Error: the source path does not exist.', 'Error', wx.ICON_ERROR)
return
# delete all rows:
self.clear_results()
d = wx.ProgressDialog(title='Cleaning', message='Scanning and cleaning %s\nto %s...'
% (source, dest))
d.Pulse()
try:
if os.path.isdir(source):
# dest must also be a dir:
if os.path.exists(dest):
if not os.path.isdir(dest):
wx.MessageBox('Error: if the source is a folder, the destination must also be a folder.', 'Error', wx.ICON_ERROR)
return
# else dest is created as a dir if it doesn't exist
wx.BeginBusyCursor()
xf.transfert([source], dest)
wx.EndBusyCursor()
else:
wx.BeginBusyCursor()
xf.transfert([source], dest, dest_is_a_file=True)
wx.EndBusyCursor()
#xf.display_html_report()
except:
wx.EndBusyCursor()
msg = 'Unhandled error while cleaning: please report it to decalage(a)laposte.net\n' + traceback.format_exc()
wx.MessageBox(msg, 'Error', wx.ICON_ERROR)
d.Update(value=100)
# it's necessary to destroy the dialog else the console stays open at the end
d.Destroy()
l = self.list_results
for result in Rapport.liste_resultats:
filename = result.chemin_fichier
index = l.InsertStringItem(sys.maxint, filename)
if result.code_resultat == Resultat.ACCEPTE:
text = 'CLEAN'
color = COLOR_CLEAN #wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN
elif result.code_resultat == Resultat.NETTOYE:
text = 'CLEANED'
color = COLOR_CLEANED #wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW
elif result.code_resultat == Resultat.REFUSE \
or result.code_resultat == Resultat.EXT_NON_AUTORISEE \
or result.code_resultat == Resultat.FORMAT_INCORRECT \
or result.code_resultat == Resultat.VIRUS :
text = 'BLOCKED'
color = COLOR_BLOCKED #wx.TheColourDatabase.FindColour('RED') #wx.RED
elif result.code_resultat == Resultat.ERREUR_LECTURE \
or result.code_resultat == Resultat.ERREUR_ANALYSE:
text = 'ERROR'
color = COLOR_ERROR #wx.TheColourDatabase.FindColour('ORANGE')
l.SetStringItem(index, 1, text)
if result.code_resultat == Resultat.EXT_NON_AUTORISEE:
details = result.details() + '\n'
else:
details = ''
details += '\n'.join(result.raison)
l.SetStringItem(index, 2, details)
l.SetItemBackgroundColour(index, color)
for col in range(3):
l.SetColumnWidth(col=col, width=-1)
# old code with grid widget:
## grid = self.grid_results
## # delete all rows:
## n = grid.GetNumberRows()
## if n:
## grid.DeleteRows(0,n)
## row = 0
## for result in Rapport.liste_resultats:
## grid.AppendRows(1)
## filename = result.chemin_fichier
## grid.SetCellValue(row, 0, filename)
## if result.code_resultat == Resultat.ACCEPTE:
## text = 'CLEAN'
## color = wx.TheColourDatabase.FindColour('GREEN') #wx.GREEN
## elif result.code_resultat == Resultat.NETTOYE:
## text = 'CLEANED'
## color = wx.TheColourDatabase.FindColour('YELLOW') #wx.YELLOW
## elif result.code_resultat == Resultat.REFUSE \
## or result.code_resultat == Resultat.EXT_NON_AUTORISEE \
## or result.code_resultat == Resultat.FORMAT_INCORRECT \
## or result.code_resultat == Resultat.VIRUS :
## text = 'BLOCKED'
## color = wx.TheColourDatabase.FindColour('RED') #wx.RED
## elif result.code_resultat == Resultat.ERREUR_LECTURE \
## or result.code_resultat == Resultat.ERREUR_ANALYSE:
## text = 'ERROR'
## color = wx.TheColourDatabase.FindColour('ORANGE')
## grid.SetCellValue(row, 1, text)
## grid.SetCellBackgroundColour(row, 1, color)
## if result.code_resultat == Resultat.EXT_NON_AUTORISEE:
## details = result.details() + '\n'
## else:
## details = ''
## details += '\n'.join(result.raison)
## details = details.strip()
## grid.SetCellValue(row, 2, details)
## row += 1
## grid.AutoSize()
## grid.ForceRefresh()
def quit(self, event): # wxGlade: MainFrame.<event_handler>
self.Close(True)
def load_policy(self, event): # wxGlade: MainFrame.<event_handler>
print "Event handler `load_policy' not implemented"
event.Skip()
def report_html(self, event): # wxGlade: MainFrame.<event_handler>
print "Event handler `report_html' not implemented"
event.Skip()
def report_xml(self, event): # wxGlade: MainFrame.<event_handler>
print "Event handler `report_xml' not implemented"
event.Skip()
def display_doc(self, event): # wxGlade: MainFrame.<event_handler>
os.startfile('ExeFilter_documentation_EN.pdf')
# on most Linux systems:
#os.system("/usr/bin/xdg-open ExeFilter_documentation_EN.pdf")
# on MacOSX:
#os.system("/usr/bin/open ExeFilter_documentation_EN.pdf")
# last alternative: open file URL in web browser?
def about(self, event): # wxGlade: MainFrame.<event_handler>
info = wx.AboutDialogInfo()
info.SetName('ExeFilter GUI')
info.SetVersion(xf.XF_VERSION)
#info.SetDevelopers([__author__])
info.SetDescription('A tool to scan and sanitize files, removing active content in common file formats '
'such as PDF, HTML, RTF and MS Office.')
info.SetCopyright('Copyright (C) Philippe Lagadec 2010-2011')
info.SetWebSite('http://www.decalage.info/exefilter')
info.SetLicense('CeCILL license, open-source, GPL-compatible.\n'
'See file Licence_CeCILL_V2-en.html for the full license.')
wx.AboutBox(info)
# end of class MainFrame
#=== MAIN =====================================================================
if __name__ == "__main__":
xfgui_app = wx.PySimpleApp(0)
wx.InitAllImageHandlers()
main_frame = MainFrame(None, -1, "")
xfgui_app.SetTopWindow(main_frame)
main_frame.Show()
xfgui_app.MainLoop()