1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# ***********************IMPORTANT NMAP LICENSE TERMS************************
5# *                                                                         *
6# * The Nmap Security Scanner is (C) 1996-2020 Insecure.Com LLC ("The Nmap  *
7# * Project"). Nmap is also a registered trademark of the Nmap Project.     *
8# *                                                                         *
9# * This program is distributed under the terms of the Nmap Public Source   *
10# * License (NPSL). The exact license text applying to a particular Nmap    *
11# * release or source code control revision is contained in the LICENSE     *
12# * file distributed with that version of Nmap or source code control       *
13# * revision. More Nmap copyright/legal information is available from       *
14# * https://nmap.org/book/man-legal.html, and further information on the    *
15# * NPSL license itself can be found at https://nmap.org/npsl. This header  *
16# * summarizes some key points from the Nmap license, but is no substitute  *
17# * for the actual license text.                                            *
18# *                                                                         *
19# * Nmap is generally free for end users to download and use themselves,    *
20# * including commercial use. It is available from https://nmap.org.        *
21# *                                                                         *
22# * The Nmap license generally prohibits companies from using and           *
23# * redistributing Nmap in commercial products, but we sell a special Nmap  *
24# * OEM Edition with a more permissive license and special features for     *
25# * this purpose. See https://nmap.org/oem                                  *
26# *                                                                         *
27# * If you have received a written Nmap license agreement or contract       *
28# * stating terms other than these (such as an Nmap OEM license), you may   *
29# * choose to use and redistribute Nmap under those terms instead.          *
30# *                                                                         *
31# * The official Nmap Windows builds include the Npcap software             *
32# * (https://npcap.org) for packet capture and transmission. It is under    *
33# * separate license terms which forbid redistribution without special      *
34# * permission. So the official Nmap Windows builds may not be              *
35# * redistributed without special permission (such as an Nmap OEM           *
36# * license).                                                               *
37# *                                                                         *
38# * Source is provided to this software because we believe users have a     *
39# * right to know exactly what a program is going to do before they run it. *
40# * This also allows you to audit the software for security holes.          *
41# *                                                                         *
42# * Source code also allows you to port Nmap to new platforms, fix bugs,    *
43# * and add new features.  You are highly encouraged to submit your         *
44# * changes as a Github PR or by email to the dev@nmap.org mailing list     *
45# * for possible incorporation into the main distribution. Unless you       *
46# * specify otherwise, it is understood that you are offering us very       *
47# * broad rights to use your submissions as described in the Nmap Public    *
48# * Source License Contributor Agreement. This is important because we      *
49# * fund the project by selling licenses with various terms, and also       *
50# * because the inability to relicense code has caused devastating          *
51# * problems for other Free Software projects (such as KDE and NASM).       *
52# *                                                                         *
53# * The free version of Nmap is distributed in the hope that it will be     *
54# * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of  *
55# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Warranties,        *
56# * indemnification and commercial support are all available through the    *
57# * Npcap OEM program--see https://nmap.org/oem.                            *
58# *                                                                         *
59# ***************************************************************************/
60
61import gtk
62
63import sys
64import os
65from os.path import split, isfile, join, abspath
66
67# Prevent loading PyXML
68import xml
69xml.__path__ = [x for x in xml.__path__ if "_xmlplus" not in x]
70
71import xml.sax.saxutils
72
73from zenmapGUI.higwidgets.higwindows import HIGMainWindow
74from zenmapGUI.higwidgets.higdialogs import HIGDialog, HIGAlertDialog
75from zenmapGUI.higwidgets.higlabels import HIGEntryLabel
76from zenmapGUI.higwidgets.higboxes import HIGHBox, HIGVBox
77
78import zenmapGUI.App
79from zenmapGUI.FileChoosers import RESPONSE_OPEN_DIRECTORY, \
80        ResultsFileChooserDialog, SaveResultsFileChooserDialog, \
81        SaveToDirectoryChooserDialog
82from zenmapGUI.ScanInterface import ScanInterface
83from zenmapGUI.ProfileEditor import ProfileEditor
84from zenmapGUI.About import About
85from zenmapGUI.DiffCompare import DiffWindow
86from zenmapGUI.SearchWindow import SearchWindow
87from zenmapGUI.BugReport import BugReport
88
89from zenmapCore.Name import APP_DISPLAY_NAME, APP_DOCUMENTATION_SITE
90from zenmapCore.BasePaths import fs_enc
91from zenmapCore.Paths import Path
92from zenmapCore.RecentScans import recent_scans
93from zenmapCore.UmitLogging import log
94import zenmapCore.I18N  # lgtm[py/unused-import]
95import zenmapGUI.Print
96from zenmapCore.UmitConf import SearchConfig, is_maemo, WindowConfig, config_parser
97
98UmitScanWindow = None
99hildon = None
100
101if is_maemo():
102    import hildon
103
104    class UmitScanWindow(hildon.Window):
105        def __init__(self):
106            hildon.Window.__init__(self)
107            self.set_resizable(False)
108            self.set_border_width(0)
109            self.vbox = gtk.VBox()
110            self.vbox.set_border_width(0)
111            self.vbox.set_spacing(0)
112
113else:
114    class UmitScanWindow(HIGMainWindow):
115        def __init__(self):
116            HIGMainWindow.__init__(self)
117            self.vbox = gtk.VBox()
118
119
120def can_print():
121    """Return true if we have printing operations (PyGTK 2.10 or later) or
122    false otherwise."""
123    try:
124        gtk.PrintOperation
125    except AttributeError:
126        return False
127    else:
128        return True
129
130
131class ScanWindow(UmitScanWindow):
132    def __init__(self):
133        UmitScanWindow.__init__(self)
134
135        window = WindowConfig()
136
137        self.set_title(_(APP_DISPLAY_NAME))
138        self.move(window.x, window.y)
139        self.set_default_size(window.width, window.height)
140
141        self.scan_interface = ScanInterface()
142
143        self.main_accel_group = gtk.AccelGroup()
144
145        self.add_accel_group(self.main_accel_group)
146
147        # self.vbox is a container for the menubar and the scan interface
148        self.add(self.vbox)
149
150        self.connect('delete-event', self._exit_cb)
151        self._create_ui_manager()
152        self._create_menubar()
153        self._create_scan_interface()
154
155        self._results_filechooser_dialog = None
156        self._about_dialog = None
157
158    def _create_ui_manager(self):
159        """Creates the UI Manager and a default set of actions, and builds
160        the menus using those actions."""
161        self.ui_manager = gtk.UIManager()
162
163        # See info on ActionGroup at:
164        # * http://www.pygtk.org/pygtk2reference/class-gtkactiongroup.html
165        # * http://www.gtk.org/api/2.6/gtk/GtkActionGroup.html
166        self.main_action_group = gtk.ActionGroup('MainActionGroup')
167
168        # See info on Action at:
169        # * http://www.pygtk.org/pygtk2reference/class-gtkaction.html
170        # * http://www.gtk.org/api/2.6/gtk/GtkAction.html
171
172        # Each action tuple can go from 1 to six fields, example:
173        # ('Open Scan Results',      -> Name of the action
174        #   gtk.STOCK_OPEN,          ->
175        #   _('_Open Scan Results'), ->
176        #   None,
177        #   _('Open the results of a previous scan'),
178        #   lambda x: True)
179
180        # gtk.STOCK_ABOUT is only available in PyGTK 2.6 and later.
181        try:
182            about_icon = gtk.STOCK_ABOUT
183        except AttributeError:
184            about_icon = None
185
186        self.main_actions = [
187            # Top level
188            ('Scan', None, _('Sc_an'), None),
189
190            ('Save Scan',
191                gtk.STOCK_SAVE,
192                _('_Save Scan'),
193                None,
194                _('Save current scan results'),
195                self._save_scan_results_cb),
196
197            ('Save All Scans to Directory',
198                gtk.STOCK_SAVE,
199                _('Save All Scans to _Directory'),
200                "<Control><Alt>s",
201                _('Save all scans into a directory'),
202                self._save_to_directory_cb),
203
204            ('Open Scan',
205                gtk.STOCK_OPEN,
206                _('_Open Scan'),
207                None,
208                _('Open the results of a previous scan'),
209                self._load_scan_results_cb),
210
211            ('Append Scan',
212                gtk.STOCK_ADD,
213                _('_Open Scan in This Window'),
214                None,
215                _('Append a saved scan to the list of scans in this window.'),
216                self._append_scan_results_cb),
217
218
219            ('Tools', None, _('_Tools'), None),
220
221            ('New Window',
222                gtk.STOCK_NEW,
223                _('_New Window'),
224                "<Control>N",
225                _('Open a new scan window'),
226                self._new_scan_cb),
227
228            ('Close Window',
229                gtk.STOCK_CLOSE,
230                _('Close Window'),
231                "<Control>w",
232                _('Close this scan window'),
233                self._exit_cb),
234
235            ('Print...',
236                gtk.STOCK_PRINT,
237                _('Print...'),
238                None,
239                _('Print the current scan'),
240                self._print_cb),
241
242            ('Quit',
243                gtk.STOCK_QUIT,
244                _('Quit'),
245                "<Control>q",
246                _('Quit the application'),
247                self._quit_cb),
248
249            ('New Profile',
250                gtk.STOCK_JUSTIFY_LEFT,
251                _('New _Profile or Command'),
252                '<Control>p',
253                _('Create a new scan profile using the current command'),
254                self._new_scan_profile_cb),
255
256            ('Search Scan',
257                gtk.STOCK_FIND,
258                _('Search Scan Results'),
259                '<Control>f',
260                _('Search for a scan result'),
261                self._search_scan_result),
262
263            ('Filter Hosts',
264                gtk.STOCK_FIND,
265                _('Filter Hosts'),
266                '<Control>l',
267                _('Search for host by criteria'),
268                self._filter_cb),
269
270            ('Edit Profile',
271                gtk.STOCK_PROPERTIES,
272                _('_Edit Selected Profile'),
273                '<Control>e',
274                _('Edit selected scan profile'),
275                self._edit_scan_profile_cb),
276
277            # Top Level
278            ('Profile', None, _('_Profile'), None),
279
280            ('Compare Results',
281                gtk.STOCK_DND_MULTIPLE,
282                _('Compare Results'),
283                "<Control>D",
284                _('Compare Scan Results using Diffies'),
285                self._load_diff_compare_cb),
286
287
288            # Top Level
289            ('Help', None, _('_Help'), None),
290
291            ('Report a bug',
292                gtk.STOCK_DIALOG_INFO,
293                _('_Report a bug'),
294                '<Control>b',
295                _("Report a bug"),
296                self._show_bug_report
297                ),
298
299            ('About',
300                about_icon,
301                _('_About'),
302                None,
303                _("About %s") % APP_DISPLAY_NAME,
304                self._show_about_cb
305                ),
306
307            ('Show Help',
308                gtk.STOCK_HELP,
309                _('_Help'),
310                None,
311                _('Shows the application help'),
312                self._help_cb),
313            ]
314
315        # See info on UIManager at:
316        # * http://www.pygtk.org/pygtk2reference/class-gtkuimanager.html
317        # * http://www.gtk.org/api/2.6/gtk/GtkUIManager.html
318
319        # UIManager supports UI "merging" and "unmerging". So, suppose there's
320        # no scan running or scan results opened, we should have a minimal
321        # interface. When we one scan running, we should "merge" the scan UI.
322        # When we get multiple tabs opened, we might merge the tab UI.
323
324        # This is the default, minimal UI
325        self.default_ui = """<menubar>
326        <menu action='Scan'>
327            <menuitem action='New Window'/>
328            <menuitem action='Open Scan'/>
329            <menuitem action='Append Scan'/>
330             %s
331            <separator/>
332            <menuitem action='Save Scan'/>
333            <menuitem action='Save All Scans to Directory'/>
334        """
335        if can_print():
336            self.default_ui += """
337            <separator/>
338            <menuitem action='Print...'/>
339            """
340        self.default_ui += """
341            <separator/>
342            <menuitem action='Close Window'/>
343            <menuitem action='Quit'/>
344        </menu>
345
346        <menu action='Tools'>
347            <menuitem action='Compare Results'/>
348            <menuitem action='Search Scan'/>
349            <menuitem action='Filter Hosts'/>
350         </menu>
351
352        <menu action='Profile'>
353            <menuitem action='New Profile'/>
354            <menuitem action='Edit Profile'/>
355        </menu>
356
357        <menu action='Help'>
358            <menuitem action='Show Help'/>
359            <menuitem action='Report a bug'/>
360            <menuitem action='About'/>
361        </menu>
362
363        </menubar>
364        """
365
366        self.get_recent_scans()
367
368        self.main_action_group.add_actions(self.main_actions)
369
370        for action in self.main_action_group.list_actions():
371            action.set_accel_group(self.main_accel_group)
372            action.connect_accelerator()
373
374        self.ui_manager.insert_action_group(self.main_action_group, 0)
375        self.ui_manager.add_ui_from_string(self.default_ui)
376
377    def _show_bug_report(self, widget):
378        """Displays a 'How to report a bug' window."""
379        bug = BugReport()
380        bug.show_all()
381
382    def _search_scan_result(self, widget):
383        """Displays a search window."""
384        search_window = SearchWindow(
385                self._load_search_result, self._append_search_result)
386        search_window.show_all()
387
388    def _filter_cb(self, widget):
389        self.scan_interface.toggle_filter_bar()
390
391    def _load_search_result(self, results):
392        """This function is passed as an argument to the SearchWindow.__init__
393        method.  When the user selects scans in the search window and clicks on
394        "Open", this function is called to load each of the selected scans into
395        a new window."""
396        for result in results:
397            self._load(self.get_empty_interface(),
398                    parsed_result=results[result][1])
399
400    def _append_search_result(self, results):
401        """This function is passed as an argument to the SearchWindow.__init__
402        method.  When the user selects scans in the search window and clicks on
403        "Append", this function is called to append the selected scans into the
404        current window."""
405        for result in results:
406            self._load(self.scan_interface, parsed_result=results[result][1])
407
408    def store_result(self, scan_interface):
409        """Stores the network inventory into the database."""
410        log.debug(">>> Saving result into database...")
411        try:
412            scan_interface.inventory.save_to_db()
413        except Exception, e:
414            alert = HIGAlertDialog(
415                    message_format=_("Can't save to database"),
416                    secondary_text=_("Can't store unsaved scans to the "
417                        "recent scans database:\n%s") % str(e))
418            alert.run()
419            alert.destroy()
420            log.debug(">>> Can't save result to database: %s." % str(e))
421
422    def get_recent_scans(self):
423        """Gets seven most recent scans and appends them to the default UI
424        definition."""
425        r_scans = recent_scans.get_recent_scans_list()
426        new_rscan_xml = ''
427
428        for scan in r_scans[:7]:
429            scan = scan.replace('\n', '')
430            if os.access(split(scan)[0], os.R_OK) and isfile(scan):
431                scan = scan.replace('\n', '')
432                new_rscan = (
433                        scan, None, scan, None, scan, self._load_recent_scan)
434                new_rscan_xml += "<menuitem action=%s/>\n" % (
435                        xml.sax.saxutils.quoteattr(scan))
436
437                self.main_actions.append(new_rscan)
438
439        new_rscan_xml += "<separator />\n"
440
441        self.default_ui %= new_rscan_xml
442
443    def _create_menubar(self):
444        # Get and pack the menubar
445        menubar = self.ui_manager.get_widget('/menubar')
446
447        if is_maemo():
448            menu = gtk.Menu()
449            for child in menubar.get_children():
450                child.reparent(menu)
451            self.set_menu(menu)
452            menubar.destroy()
453            self.menubar = menu
454        else:
455            self.menubar = menubar
456            self.vbox.pack_start(self.menubar, False, False, 0)
457
458        self.menubar.show_all()
459
460    def _create_scan_interface(self):
461        notebook = self.scan_interface.scan_result.scan_result_notebook
462        notebook.scans_list.append_button.connect(
463                "clicked", self._append_scan_results_cb)
464        notebook.nmap_output.connect("changed", self._displayed_scan_change_cb)
465        self._displayed_scan_change_cb(None)
466        self.scan_interface.show_all()
467        self.vbox.pack_start(self.scan_interface, True, True, 0)
468
469    def show_open_dialog(self, title=None):
470        """Show a load file chooser and return the filename chosen."""
471        if self._results_filechooser_dialog is None:
472            self._results_filechooser_dialog = ResultsFileChooserDialog(
473                    title=title)
474
475        filename = None
476        response = self._results_filechooser_dialog.run()
477        if response == gtk.RESPONSE_OK:
478            filename = self._results_filechooser_dialog.get_filename()
479        elif response == RESPONSE_OPEN_DIRECTORY:
480            filename = self._results_filechooser_dialog.get_filename()
481
482            # Check if the selected filename is a directory. If not, we take
483            # only the directory part of the path, omitting the actual name of
484            # the selected file.
485            if filename is not None and not os.path.isdir(filename):
486                filename = os.path.dirname(filename)
487
488        self._results_filechooser_dialog.hide()
489        return filename
490
491    def _load_scan_results_cb(self, p):
492        """'Open Scan' callback function. Displays a file chooser dialog and
493        loads the scan from the selected file or from the selected
494        directory."""
495        filename = self.show_open_dialog(p.get_name())
496        if filename is not None:
497            scan_interface = self.get_empty_interface()
498            if os.path.isdir(filename):
499                self._load_directory(scan_interface, filename)
500            else:
501                self._load(scan_interface, filename)
502
503    def _append_scan_results_cb(self, p):
504        """'Append Scan' callback function. Displays a file chooser dialog and
505        appends the scan from the selected file into the current window."""
506        filename = self.show_open_dialog(p.get_name())
507        if filename is not None:
508            if os.path.isdir(filename):
509                self._load_directory(self.scan_interface, filename)
510            else:
511                self._load(self.scan_interface, filename)
512
513    def _displayed_scan_change_cb(self, widget):
514        """Called when the currently shown scan output is changed."""
515        # Set the Print... menu item sensitive if there is something to print.
516        widget = self.ui_manager.get_widget("/ui/menubar/Scan/Print...")
517        if widget is None:
518            # Don't have a Print menu item for lack of support.
519            return
520        entry = self.scan_interface.scan_result.scan_result_notebook.nmap_output.get_active_entry()  # noqa
521        widget.set_sensitive(entry is not None)
522
523    def _load_recent_scan(self, widget):
524        """A helper function for loading a recent scan directly from the
525        menu."""
526        self._load(self.get_empty_interface(), widget.get_name())
527
528    def _load(self, scan_interface, filename=None, parsed_result=None):
529        """Loads the scan from a file or from a parsed result into the given
530        scan interface."""
531        if not (filename or parsed_result):
532            return None
533
534        if filename:
535            # Load scan result from file
536            log.debug(">>> Loading file: %s" % filename)
537            try:
538                # Parse result
539                scan_interface.load_from_file(filename)
540            except Exception, e:
541                alert = HIGAlertDialog(message_format=_('Error loading file'),
542                                       secondary_text=str(e))
543                alert.run()
544                alert.destroy()
545                return
546            scan_interface.saved_filename = filename
547        elif parsed_result:
548            # Load scan result from parsed object
549            scan_interface.load_from_parsed_result(parsed_result)
550
551    def _load_directory(self, scan_interface, directory):
552        for file in os.listdir(directory):
553            if os.path.isdir(os.path.join(directory, file)):
554                continue
555            self._load(scan_interface, filename=os.path.join(directory, file))
556
557    def _save_scan_results_cb(self, widget):
558        """'Save Scan' callback function. If it's OK to save the scan, it
559        displays a 'Save File' dialog and saves the scan. If not, it displays
560        an appropriate alert dialog."""
561        num_scans = len(self.scan_interface.inventory.get_scans())
562        if num_scans == 0:
563            alert = HIGAlertDialog(
564                    message_format=_('Nothing to save'),
565                    secondary_text=_(
566                        'There are no scans with results to be saved. '
567                        'Run a scan with the "Scan" button first.'))
568            alert.run()
569            alert.destroy()
570            return
571        num_scans_running = self.scan_interface.num_scans_running()
572        if num_scans_running > 0:
573            if num_scans_running == 1:
574                text = _("There is a scan still running. "
575                        "Wait until it finishes and then save.")
576            else:
577                text = _("There are %u scans still running. Wait until they "
578                        "finish and then save.") % num_scans_running
579            alert = HIGAlertDialog(message_format=_('Scan is running'),
580                                   secondary_text=text)
581            alert.run()
582            alert.destroy()
583            return
584
585        # If there's more than one scan in the inventory, display a warning
586        # dialog saying that only the most recent scan will be saved
587        selected = 0
588        if num_scans > 1:
589            #text = _("You have %u scans loaded in the current view. "
590            #        "Only the most recent scan will be saved." % num_scans)
591            #alert = HIGAlertDialog(
592            #        message_format=_("More than one scan loaded"),
593            #       secondary_text=text)
594            #alert.run()
595            #alert.destroy()
596            dlg = HIGDialog(
597                    title="Choose a scan to save",
598                    parent=self,
599                    flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
600                    buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
601                        gtk.STOCK_SAVE, gtk.RESPONSE_OK))
602            dlg.vbox.pack_start(gtk.Label(
603                "You have %u scans loaded in the current view.\n"
604                "Select the scan which you would like to save." % num_scans),
605                False)
606            scan_combo = gtk.combo_box_new_text()
607            for scan in self.scan_interface.inventory.get_scans():
608                scan_combo.append_text(scan.nmap_command)
609            scan_combo.set_active(0)
610            dlg.vbox.pack_start(scan_combo, False)
611            dlg.vbox.show_all()
612            if dlg.run() == gtk.RESPONSE_OK:
613                selected = scan_combo.get_active()
614                dlg.destroy()
615            else:
616                dlg.destroy()
617                return
618
619        # Show the dialog to choose the path to save scan result
620        self._save_results_filechooser_dialog = \
621            SaveResultsFileChooserDialog(title=_('Save Scan'))
622        # Supply a default file name if this scan was previously saved.
623        if self.scan_interface.saved_filename:
624            self._save_results_filechooser_dialog.set_filename(
625                    self.scan_interface.saved_filename)
626
627        response = self._save_results_filechooser_dialog.run()
628
629        filename = None
630        if (response == gtk.RESPONSE_OK):
631            filename = self._save_results_filechooser_dialog.get_filename()
632            format = self._save_results_filechooser_dialog.get_format()
633            # add .xml to filename if there is no other extension
634            if filename.find('.') == -1:
635                filename += ".xml"
636            self._save(self.scan_interface, filename, selected, format)
637
638        self._save_results_filechooser_dialog.destroy()
639        self._save_results_filechooser_dialog = None
640
641    def _save_to_directory_cb(self, widget):
642        if self.scan_interface.empty:
643            alert = HIGAlertDialog(message_format=_('Nothing to save'),
644                                   secondary_text=_('\
645This scan has not been run yet. Start the scan with the "Scan" button first.'))
646            alert.run()
647            alert.destroy()
648            return
649        num_scans_running = self.scan_interface.num_scans_running()
650        if num_scans_running > 0:
651            if num_scans_running == 1:
652                text = _("There is a scan still running. "
653                        "Wait until it finishes and then save.")
654            else:
655                text = _("There are %u scans still running. Wait until they "
656                        "finish and then save.") % num_scans_running
657            alert = HIGAlertDialog(message_format=_('Scan is running'),
658                                   secondary_text=text)
659            alert.run()
660            alert.destroy()
661            return
662
663        # We have multiple scans in our network inventory, so we need to
664        # display a directory chooser dialog
665        dir_chooser = SaveToDirectoryChooserDialog(
666                title=_("Choose a directory to save scans into"))
667        if dir_chooser.run() == gtk.RESPONSE_OK:
668            self._save_all(self.scan_interface, dir_chooser.get_filename())
669        dir_chooser.destroy()
670
671    def _about_cb_response(self, dialog, response_id):
672        if response_id == gtk.RESPONSE_DELETE_EVENT:
673            self._about_dialog = None
674        else:
675            self._about_dialog.hide()
676
677    def _show_about_cb(self, widget):
678        if self._about_dialog is None:
679            self._about_dialog = About()
680            self._about_dialog.connect("response", self._about_cb_response)
681        self._about_dialog.present()
682
683    def _save_all(self, scan_interface, directory):
684        """Saves all scans in saving_page's inventory to a given directory.
685        Displays an alert dialog if the save fails."""
686        try:
687            filenames = scan_interface.inventory.save_to_dir(directory)
688            for scan in scan_interface.inventory.get_scans():
689                scan.unsaved = False
690        except Exception, ex:
691            alert = HIGAlertDialog(message_format=_('Can\'t save file'),
692                        secondary_text=str(ex))
693            alert.run()
694            alert.destroy()
695        else:
696            scan_interface.saved_filename = directory
697
698            # Saving recent scan information
699            try:
700                for filename in filenames:
701                    recent_scans.add_recent_scan(filename)
702                recent_scans.save()
703            except (OSError, IOError), e:
704                alert = HIGAlertDialog(
705                        message_format=_(
706                            "Can't save recent scan information"),
707                        secondary_text=_(
708                            "Can't open file to write.\n%s") % str(e))
709                alert.run()
710                alert.destroy()
711
712    def _save(self, scan_interface, saved_filename, selected_index,
713            format="xml"):
714        """Saves the scan into a file with a given filename. Displays an alert
715        dialog if the save fails."""
716        log.debug(">>> File being saved: %s" % saved_filename)
717        try:
718            scan_interface.inventory.save_to_file(
719                    saved_filename, selected_index, format)
720            scan_interface.inventory.get_scans()[selected_index].unsaved = False  # noqa
721        except (OSError, IOError), e:
722            alert = HIGAlertDialog(
723                    message_format=_("Can't save file"),
724                    secondary_text=_("Can't open file to write.\n%s") % str(e))
725            alert.run()
726            alert.destroy()
727        else:
728            scan_interface.saved_filename = saved_filename
729
730            log.debug(">>> Changes on page? %s" % scan_interface.changed)
731            log.debug(">>> File saved at: %s" % scan_interface.saved_filename)
732
733            if format == "xml":
734                # Saving recent scan information
735                try:
736                    recent_scans.add_recent_scan(saved_filename)
737                    recent_scans.save()
738                except (OSError, IOError), e:
739                    alert = HIGAlertDialog(
740                            message_format=_(
741                                "Can't save recent scan information"),
742                            secondary_text=_(
743                                "Can't open file to write.\n%s") % str(e))
744                    alert.run()
745                    alert.destroy()
746
747    def get_empty_interface(self):
748        """Return this window if it is empty, otherwise create and return a new
749        one."""
750        if self.scan_interface.empty:
751            return self.scan_interface
752        return self._new_scan_cb().scan_interface
753
754    def _new_scan_cb(self, widget=None, data=None):
755        """Create a new scan window."""
756        w = zenmapGUI.App.new_window()
757        w.show_all()
758        return w
759
760    def _new_scan_profile_cb(self, p):
761        pe = ProfileEditor(
762                command=self.scan_interface.command_toolbar.command,
763                deletable=False)
764        pe.set_scan_interface(self.scan_interface)
765        pe.show_all()
766
767    def _edit_scan_profile_cb(self, p):
768        pe = ProfileEditor(
769                profile_name=self.scan_interface.toolbar.selected_profile,
770                deletable=True, overwrite=True)
771        pe.set_scan_interface(self.scan_interface)
772        pe.show_all()
773
774    def _help_cb(self, action):
775        show_help()
776
777    def _exit_cb(self, *args):
778        """Closes the window, prompting for confirmation if necessary. If one
779        of the tabs couldn't be closed, the function returns True and doesn't
780        exit the application."""
781        if self.scan_interface.changed:
782            log.debug("Found changes on closing window")
783            dialog = HIGDialog(
784                    buttons=(_('Close anyway').encode('utf-8'),
785                        gtk.RESPONSE_CLOSE, gtk.STOCK_CANCEL,
786                        gtk.RESPONSE_CANCEL))
787
788            alert = HIGEntryLabel('<b>%s</b>' % _("Unsaved changes"))
789
790            text = HIGEntryLabel(_("The given scan has unsaved changes.\n"
791                "What do you want to do?"))
792            hbox = HIGHBox()
793            hbox.set_border_width(5)
794            hbox.set_spacing(12)
795
796            vbox = HIGVBox()
797            vbox.set_border_width(5)
798            vbox.set_spacing(12)
799
800            image = gtk.Image()
801            image.set_from_stock(
802                    gtk.STOCK_DIALOG_QUESTION, gtk.ICON_SIZE_DIALOG)
803
804            vbox.pack_start(alert)
805            vbox.pack_start(text)
806            hbox.pack_start(image)
807            hbox.pack_start(vbox)
808
809            dialog.vbox.pack_start(hbox)
810            dialog.vbox.show_all()
811
812            response = dialog.run()
813            dialog.destroy()
814
815            if response == gtk.RESPONSE_CANCEL:
816                return True
817
818            search_config = SearchConfig()
819            if search_config.store_results:
820                self.store_result(self.scan_interface)
821
822        elif self.scan_interface.num_scans_running() > 0:
823            log.debug("Trying to close a window with a running scan")
824            dialog = HIGDialog(
825                    buttons=(_('Close anyway').encode('utf-8'),
826                        gtk.RESPONSE_CLOSE, gtk.STOCK_CANCEL,
827                        gtk.RESPONSE_CANCEL))
828
829            alert = HIGEntryLabel('<b>%s</b>' % _("Trying to close"))
830
831            text = HIGEntryLabel(_(
832                "The window you are trying to close has a scan running in "
833                "the background.\nWhat do you want to do?"))
834            hbox = HIGHBox()
835            hbox.set_border_width(5)
836            hbox.set_spacing(12)
837
838            vbox = HIGVBox()
839            vbox.set_border_width(5)
840            vbox.set_spacing(12)
841
842            image = gtk.Image()
843            image.set_from_stock(
844                    gtk.STOCK_DIALOG_WARNING, gtk.ICON_SIZE_DIALOG)
845
846            vbox.pack_start(alert)
847            vbox.pack_start(text)
848            hbox.pack_start(image)
849            hbox.pack_start(vbox)
850
851            dialog.vbox.pack_start(hbox)
852            dialog.vbox.show_all()
853
854            response = dialog.run()
855            dialog.destroy()
856
857            if response == gtk.RESPONSE_CLOSE:
858                self.scan_interface.kill_all_scans()
859            elif response == gtk.RESPONSE_CANCEL:
860                return True
861
862        window = WindowConfig()
863        window.x, window.y = self.get_position()
864        window.width, window.height = self.get_size()
865        window.save_changes()
866        if config_parser.failed:
867            alert = HIGAlertDialog(
868                    message_format=_("Can't save Zenmap configuration"),
869                    # newline before path to help avoid weird line wrapping
870                    secondary_text=_(
871                        'An error occurred when saving to\n%s'
872                        '\nThe error was: %s.'
873                        ) % (Path.user_config_file, config_parser.failed))
874            alert.run()
875            alert.destroy()
876
877        self.destroy()
878
879        return False
880
881    def _print_cb(self, *args):
882        """Show a print dialog."""
883        entry = self.scan_interface.scan_result.scan_result_notebook.nmap_output.get_active_entry()  # noqa
884        if entry is None:
885            return False
886        zenmapGUI.Print.run_print_operation(
887                self.scan_interface.inventory, entry)
888
889    def _quit_cb(self, *args):
890        """Close all open windows."""
891        for window in zenmapGUI.App.open_windows[:]:
892            window.present()
893            if window._exit_cb():
894                break
895
896    def _load_diff_compare_cb(self, widget=None, extra=None):
897        """Loads all active scans into a dictionary, passes it to the
898        DiffWindow constructor, and then displays the 'Compare Results'
899        window."""
900        self.diff_window = DiffWindow(
901                self.scan_interface.inventory.get_scans())
902        self.diff_window.show_all()
903
904
905def show_help():
906    import urllib
907    import webbrowser
908
909    new = 0
910    if sys.hexversion >= 0x2050000:
911        new = 2
912
913    doc_path = abspath(join(Path.docs_dir, "help.html"))
914    url = "file:" + urllib.pathname2url(fs_enc(doc_path))
915    try:
916        webbrowser.open(url, new=new)
917    except OSError, e:
918        d = HIGAlertDialog(parent=self,
919                           message_format=_("Can't find documentation files"),
920                           secondary_text=_("""\
921There was an error loading the documentation file %s (%s). See the \
922online documentation at %s.\
923""") % (doc_path, unicode(e), APP_DOCUMENTATION_SITE))
924        d.run()
925        d.destroy()
926
927if __name__ == '__main__':
928    w = ScanWindow()
929    w.show_all()
930    gtk.main()
931