class GraphBase(object): """ A basic graph provider for using :py:mod:`matplotlib` to create graph representations of campaign data. This class is meant to be subclassed by real providers. """ name = 'Unknown' """The name of the graph provider.""" name_human = 'Unknown' """The human readable name of the graph provider used for UI identification.""" graph_title = 'Unknown' """The title that will be given to the graph.""" table_subscriptions = [] """A list of tables from which information is needed to produce the graph.""" is_available = True def __init__(self, application, size_request=None, style_context=None): """ :param tuple size_request: The size to set for the canvas. """ self.application = application self.style_context = style_context self.config = application.config """A reference to the King Phisher client configuration.""" self.figure, _ = pyplot.subplots() self.figure.set_facecolor(self.get_color('bg', ColorHexCode.WHITE)) self.axes = self.figure.get_axes() self.canvas = FigureCanvas(self.figure) self.manager = None self.minimum_size = (380, 200) """An absolute minimum size for the canvas.""" if size_request is not None: self.resize(*size_request) self.canvas.mpl_connect('button_press_event', self.mpl_signal_canvas_button_pressed) self.canvas.show() self.navigation_toolbar = NavigationToolbar( self.canvas, self.application.get_active_window()) self.popup_menu = Gtk.Menu.new() menu_item = Gtk.MenuItem.new_with_label('Export') menu_item.connect('activate', self.signal_activate_popup_menu_export) self.popup_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('Refresh') menu_item.connect('activate', self.signal_activate_popup_refresh) self.popup_menu.append(menu_item) menu_item = Gtk.CheckMenuItem.new_with_label('Show Toolbar') menu_item.connect('toggled', self.signal_toggled_popup_menu_show_toolbar) self._menu_item_show_toolbar = menu_item self.popup_menu.append(menu_item) self.popup_menu.show_all() self.navigation_toolbar.hide() self._legend = None @property def rpc(self): return self.application.rpc @staticmethod def _ax_hide_ticks(ax): for tick in ax.yaxis.get_major_ticks(): tick.tick1On = False tick.tick2On = False @staticmethod def _ax_set_spine_color(ax, spine_color): for pos in ('top', 'right', 'bottom', 'left'): ax.spines[pos].set_color(spine_color) def add_legend_patch(self, legend_rows, fontsize=None): if self._legend is not None: self._legend.remove() self._legend = None fontsize = fontsize or self.fontsize_scale legend_bbox = self.figure.legend(tuple( patches.Patch(color=patch_color) for patch_color, _ in legend_rows), tuple(label for _, label in legend_rows), borderaxespad=1.25, fontsize=fontsize, frameon=True, handlelength=1.5, handletextpad=0.75, labelspacing=0.3, loc='lower right') legend_bbox.legendPatch.set_linewidth(0) self._legend = legend_bbox def get_color(self, color_name, default): """ Get a color by its style name such as 'fg' for foreground. If the specified color does not exist, default will be returned. The underlying logic for this function is provided by :py:func:`~.gui_utilities.gtk_style_context_get_color`. :param str color_name: The style name of the color. :param default: The default color to return if the specified one was not found. :return: The desired color if it was found. :rtype: tuple """ color_name = 'theme_color_graph_' + color_name sc_color = gui_utilities.gtk_style_context_get_color( self.style_context, color_name, default) return (sc_color.red, sc_color.green, sc_color.blue) def make_window(self): """ Create a window from the figure manager. :return: The graph in a new, dedicated window. :rtype: :py:class:`Gtk.Window` """ if self.manager is None: self.manager = FigureManager(self.canvas, 0) self.navigation_toolbar.destroy() self.navigation_toolbar = self.manager.toolbar self._menu_item_show_toolbar.set_active(True) window = self.manager.window window.set_transient_for(self.application.get_active_window()) window.set_title(self.graph_title) return window @property def fontsize_scale(self): scale = self.markersize_scale if scale < 5: fontsize = 'xx-small' elif scale < 7: fontsize = 'x-small' elif scale < 9: fontsize = 'small' else: fontsize = 'medium' return fontsize @property def markersize_scale(self): bbox = self.axes[0].get_window_extent().transformed( self.figure.dpi_scale_trans.inverted()) return bbox.width * self.figure.dpi * 0.01 def mpl_signal_canvas_button_pressed(self, event): if event.button != 3: return self.popup_menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time()) return True def signal_activate_popup_menu_export(self, action): dialog = extras.FileChooserDialog('Export Graph', self.application.get_active_window()) file_name = self.config['campaign_name'] + '.png' response = dialog.run_quick_save(file_name) dialog.destroy() if not response: return destination_file = response['target_path'] self.figure.savefig(destination_file, dpi=200, facecolor=self.figure.get_facecolor(), format='png') def signal_activate_popup_refresh(self, event): self.refresh() def signal_toggled_popup_menu_show_toolbar(self, widget): if widget.get_property('active'): self.navigation_toolbar.show() else: self.navigation_toolbar.hide() def resize(self, width=0, height=0): """ Attempt to resize the canvas. Regardless of the parameters the canvas will never be resized to be smaller than :py:attr:`.minimum_size`. :param int width: The desired width of the canvas. :param int height: The desired height of the canvas. """ min_width, min_height = self.minimum_size width = max(width, min_width) height = max(height, min_height) self.canvas.set_size_request(width, height)
class GraphBase(object): """ A basic graph provider for using :py:mod:`matplotlib` to create graph representations of campaign data. This class is meant to be subclassed by real providers. """ name = 'Unknown' """The name of the graph provider.""" name_human = 'Unknown' """The human readable name of the graph provider used for UI identification.""" graph_title = 'Unknown' """The title that will be given to the graph.""" is_available = True def __init__(self, application, size_request=None, style_context=None): """ :param tuple size_request: The size to set for the canvas. """ self.application = application self.style_context = style_context self.config = application.config """A reference to the King Phisher client configuration.""" self.figure, _ = pyplot.subplots() self.figure.set_facecolor(self.get_color('bg', ColorHexCode.WHITE)) self.axes = self.figure.get_axes() self.canvas = FigureCanvas(self.figure) self.manager = None self.minimum_size = (380, 200) """An absolute minimum size for the canvas.""" if size_request is not None: self.resize(*size_request) self.canvas.mpl_connect('button_press_event', self.mpl_signal_canvas_button_pressed) self.canvas.show() self.navigation_toolbar = NavigationToolbar(self.canvas, self.application.get_active_window()) self.popup_menu = managers.MenuManager() self.popup_menu.append('Export', self.signal_activate_popup_menu_export) self.popup_menu.append('Refresh', self.signal_activate_popup_refresh) menu_item = Gtk.CheckMenuItem.new_with_label('Show Toolbar') menu_item.connect('toggled', self.signal_toggled_popup_menu_show_toolbar) self._menu_item_show_toolbar = menu_item self.popup_menu.append_item(menu_item) self.navigation_toolbar.hide() self._legend = None @property def rpc(self): return self.application.rpc @staticmethod def _ax_hide_ticks(ax): for tick in ax.yaxis.get_major_ticks(): tick.tick1On = False tick.tick2On = False @staticmethod def _ax_set_spine_color(ax, spine_color): for pos in ('top', 'right', 'bottom', 'left'): ax.spines[pos].set_color(spine_color) def add_legend_patch(self, legend_rows, fontsize=None): if self._legend is not None: self._legend.remove() self._legend = None fontsize = fontsize or self.fontsize_scale legend_bbox = self.figure.legend( tuple(patches.Patch(color=patch_color) for patch_color, _ in legend_rows), tuple(label for _, label in legend_rows), borderaxespad=1.25, fontsize=fontsize, frameon=True, handlelength=1.5, handletextpad=0.75, labelspacing=0.3, loc='lower right' ) legend_bbox.legendPatch.set_linewidth(0) self._legend = legend_bbox def get_color(self, color_name, default): """ Get a color by its style name such as 'fg' for foreground. If the specified color does not exist, default will be returned. The underlying logic for this function is provided by :py:func:`~.gui_utilities.gtk_style_context_get_color`. :param str color_name: The style name of the color. :param default: The default color to return if the specified one was not found. :return: The desired color if it was found. :rtype: tuple """ color_name = 'theme_color_graph_' + color_name sc_color = gui_utilities.gtk_style_context_get_color(self.style_context, color_name, default) return (sc_color.red, sc_color.green, sc_color.blue) def make_window(self): """ Create a window from the figure manager. :return: The graph in a new, dedicated window. :rtype: :py:class:`Gtk.Window` """ if self.manager is None: self.manager = FigureManager(self.canvas, 0) self.navigation_toolbar.destroy() self.navigation_toolbar = self.manager.toolbar self._menu_item_show_toolbar.set_active(True) window = self.manager.window window.set_transient_for(self.application.get_active_window()) window.set_title(self.graph_title) return window @property def fontsize_scale(self): scale = self.markersize_scale if scale < 5: fontsize = 'xx-small' elif scale < 7: fontsize = 'x-small' elif scale < 9: fontsize = 'small' else: fontsize = 'medium' return fontsize @property def markersize_scale(self): bbox = self.axes[0].get_window_extent().transformed(self.figure.dpi_scale_trans.inverted()) return bbox.width * self.figure.dpi * 0.01 def mpl_signal_canvas_button_pressed(self, event): if event.button != 3: return self.popup_menu.menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time()) return True def signal_activate_popup_menu_export(self, action): dialog = extras.FileChooserDialog('Export Graph', self.application.get_active_window()) file_name = self.config['campaign_name'] + '.png' response = dialog.run_quick_save(file_name) dialog.destroy() if not response: return destination_file = response['target_path'] self.figure.savefig(destination_file, dpi=200, facecolor=self.figure.get_facecolor(), format='png') def signal_activate_popup_refresh(self, event): self.refresh() def signal_toggled_popup_menu_show_toolbar(self, widget): if widget.get_property('active'): self.navigation_toolbar.show() else: self.navigation_toolbar.hide() def resize(self, width=0, height=0): """ Attempt to resize the canvas. Regardless of the parameters the canvas will never be resized to be smaller than :py:attr:`.minimum_size`. :param int width: The desired width of the canvas. :param int height: The desired height of the canvas. """ min_width, min_height = self.minimum_size width = max(width, min_width) height = max(height, min_height) self.canvas.set_size_request(width, height)
class CampaignGraph(object): """ A basic graph provider for using :py:mod:`matplotlib` to create graph representations of campaign data. This class is meant to be subclassed by real providers. """ name = 'Unknown' """The name of the graph provider.""" name_human = 'Unknown' """The human readable name of the graph provider used for UI identification.""" graph_title = 'Unknown' """The title that will be given to the graph.""" table_subscriptions = [] """A list of tables from which information is needed to produce the graph.""" is_available = True def __init__(self, config, parent, size_request=None): """ :param dict config: The King Phisher client configuration. :param parent: The parent window for this object. :type parent: :py:class:`Gtk.Window` :param tuple size_request: The size to set for the canvas. """ self.config = config """A reference to the King Phisher client configuration.""" self.parent = parent """The parent :py:class:`Gtk.Window` instance.""" self.figure, _ = pyplot.subplots() self.axes = self.figure.get_axes() self.canvas = FigureCanvas(self.figure) self.manager = None if size_request: self.canvas.set_size_request(*size_request) self.canvas.mpl_connect('button_press_event', self.mpl_signal_canvas_button_pressed) self.canvas.show() self.navigation_toolbar = NavigationToolbar(self.canvas, self.parent) self.popup_menu = Gtk.Menu.new() menu_item = Gtk.MenuItem.new_with_label('Export') menu_item.connect('activate', self.signal_activate_popup_menu_export) self.popup_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('Refresh') menu_item.connect('activate', lambda action: self.refresh()) self.popup_menu.append(menu_item) menu_item = Gtk.CheckMenuItem.new_with_label('Show Toolbar') menu_item.connect('toggled', self.signal_toggled_popup_menu_show_toolbar) self._menu_item_show_toolbar = menu_item self.popup_menu.append(menu_item) self.popup_menu.show_all() self.navigation_toolbar.hide() def _load_graph(self, info_cache): raise NotImplementedError() def _graph_bar_set_yparams(self, top_lim): min_value = top_lim + (top_lim * 0.075) if min_value <= 25: scale = 5 else: scale = scale = 10 ** (len(str(int(min_value))) - 1) inc_scale = scale while scale <= min_value: scale += inc_scale top_lim = scale ax = self.axes[0] yticks = set((round(top_lim * 0.5), top_lim)) ax.set_yticks(tuple(yticks)) ax.set_ylim(top=top_lim) return def _graph_null_pie(self, title): ax = self.axes[0] ax.pie((100,), labels=(title,), colors=(MPL_COLOR_NULL,), autopct='%1.0f%%', shadow=True, startangle=90) ax.axis('equal') return def add_legend_patch(self, legend_rows, fontsize=None): handles = [] if not fontsize: scale = self.markersize_scale if scale < 5: fontsize = 'xx-small' elif scale < 7: fontsize = 'x-small' elif scale < 9: fontsize = 'small' else: fontsize = 'medium' for row in legend_rows: handles.append(patches.Patch(color=row[0], label=row[1])) self.axes[0].legend(handles=handles, fontsize=fontsize, loc='lower right') def graph_bar(self, bars, color=None, xticklabels=None, ylabel=None): """ Create a standard bar graph with better defaults for the standard use cases. :param list bars: The values of the bars to graph. :param color: The color of the bars on the graph. :type color: list, str :param list xticklabels: The labels to use on the x-axis. :param str ylabel: The label to give to the y-axis. :return: The bars created using :py:mod:`matplotlib` :rtype: `matplotlib.container.BarContainer` """ color = color or MPL_COLOR_NULL width = 0.25 ax = self.axes[0] self._graph_bar_set_yparams(max(bars) if bars else 0) bars = ax.bar(range(len(bars)), bars, width, color=color) ax.set_xticks([float(x) + (width / 2) for x in range(len(bars))]) if xticklabels: ax.set_xticklabels(xticklabels, rotation=30) for col in bars: height = col.get_height() ax.text(col.get_x() + col.get_width() / 2.0, height, "{0:,}".format(height), ha='center', va='bottom') if ylabel: ax.set_ylabel(ylabel) self.figure.subplots_adjust(bottom=0.25) return bars def make_window(self): """ Create a window from the figure manager. :return: The graph in a new, dedicated window. :rtype: :py:class:`Gtk.Window` """ if self.manager == None: self.manager = FigureManager(self.canvas, 0) self.navigation_toolbar.destroy() self.navigation_toolbar = self.manager.toolbar self._menu_item_show_toolbar.set_active(True) window = self.manager.window window.set_transient_for(self.parent) window.set_title(self.graph_title) return window @property def markersize_scale(self): bbox = self.axes[0].get_window_extent().transformed(self.figure.dpi_scale_trans.inverted()) return max(bbox.width, bbox.width) * self.figure.dpi * 0.01 def mpl_signal_canvas_button_pressed(self, event): if event.button != 3: return self.popup_menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time()) return True def signal_activate_popup_menu_export(self, action): dialog = gui_utilities.UtilityFileChooser('Export Graph', self.parent) file_name = self.config['campaign_name'] + '.png' response = dialog.run_quick_save(file_name) dialog.destroy() if not response: return destination_file = response['target_path'] self.figure.savefig(destination_file, format='png') def signal_toggled_popup_menu_show_toolbar(self, widget): if widget.get_property('active'): self.navigation_toolbar.show() else: self.navigation_toolbar.hide() def load_graph(self): """Load the graph information via :py:meth:`.refresh`.""" self.refresh() def refresh(self, info_cache=None, stop_event=None): """ Refresh the graph data by retrieving the information from the remote server. :param dict info_cache: An optional cache of data tables. :param stop_event: An optional object indicating that the operation should stop. :type stop_event: :py:class:`threading.Event` :return: A dictionary of cached tables from the server. :rtype: dict """ info_cache = (info_cache or {}) if not self.parent.rpc: return info_cache for table in self.table_subscriptions: if stop_event and stop_event.is_set(): return info_cache if not table in info_cache: info_cache[table] = tuple(self.parent.rpc.remote_table('campaign/' + table, self.config['campaign_id'])) for ax in self.axes: ax.clear() self._load_graph(info_cache) self.axes[0].set_title(self.graph_title, y=1.03) self.canvas.draw() return info_cache
class CampaignGraph(object): """ A basic graph provider for using :py:mod:`matplotlib` to create graph representations of campaign data. This class is meant to be subclassed by real providers. """ name = 'Unknown' """The name of the graph provider.""" name_human = 'Unknown' """The human readable name of the graph provider used for UI identification.""" graph_title = 'Unknown' """The title that will be given to the graph.""" table_subscriptions = [] """A list of tables from which information is needed to produce the graph.""" is_available = True def __init__(self, application, size_request=None, style_context=None): """ :param tuple size_request: The size to set for the canvas. """ self.application = application self.style_context = style_context self.config = application.config """A reference to the King Phisher client configuration.""" self.figure, _ = pyplot.subplots() self.figure.set_facecolor(self.get_color('bg', ColorHexCode.WHITE)) self.axes = self.figure.get_axes() self.canvas = FigureCanvas(self.figure) self.manager = None if size_request: self.canvas.set_size_request(*size_request) self.canvas.mpl_connect('button_press_event', self.mpl_signal_canvas_button_pressed) self.canvas.show() self.navigation_toolbar = NavigationToolbar(self.canvas, self.application.get_active_window()) self.popup_menu = Gtk.Menu.new() menu_item = Gtk.MenuItem.new_with_label('Export') menu_item.connect('activate', self.signal_activate_popup_menu_export) self.popup_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('Refresh') menu_item.connect('activate', lambda action: self.refresh()) self.popup_menu.append(menu_item) menu_item = Gtk.CheckMenuItem.new_with_label('Show Toolbar') menu_item.connect('toggled', self.signal_toggled_popup_menu_show_toolbar) self._menu_item_show_toolbar = menu_item self.popup_menu.append(menu_item) self.popup_menu.show_all() self.navigation_toolbar.hide() self._legend = None @property def rpc(self): return self.application.rpc @staticmethod def _ax_hide_ticks(ax): for tick in ax.yaxis.get_major_ticks(): tick.tick1On = False tick.tick2On = False @staticmethod def _ax_set_spine_color(ax, spine_color): for pos in ('top', 'right', 'bottom', 'left'): ax.spines[pos].set_color(spine_color) def _load_graph(self, info_cache): raise NotImplementedError() def add_legend_patch(self, legend_rows, fontsize=None): if self._legend is not None: self._legend.remove() self._legend = None if fontsize is None: scale = self.markersize_scale if scale < 5: fontsize = 'xx-small' elif scale < 7: fontsize = 'x-small' elif scale < 9: fontsize = 'small' else: fontsize = 'medium' legend_bbox = self.figure.legend( tuple(patches.Patch(color=patch_color) for patch_color, _ in legend_rows), tuple(label for _, label in legend_rows), borderaxespad=1.25, fontsize=fontsize, frameon=True, handlelength=1.5, handletextpad=0.75, labelspacing=0.3, loc='lower right' ) legend_bbox.legendPatch.set_linewidth(0) self._legend = legend_bbox def get_color(self, color_name, default): """ Get a color by its style name such as 'fg' for foreground. If the specified color does not exist, default will be returned. The underlying logic for this function is provided by :py:func:`~.gui_utilities.gtk_style_context_get_color`. :param str color_name: The style name of the color. :param default: The default color to return if the specified one was not found. :return: The desired color if it was found. :rtype: tuple """ color_name = 'theme_color_graph_' + color_name sc_color = gui_utilities.gtk_style_context_get_color(self.style_context, color_name, default) return (sc_color.red, sc_color.green, sc_color.blue) def make_window(self): """ Create a window from the figure manager. :return: The graph in a new, dedicated window. :rtype: :py:class:`Gtk.Window` """ if self.manager is None: self.manager = FigureManager(self.canvas, 0) self.navigation_toolbar.destroy() self.navigation_toolbar = self.manager.toolbar self._menu_item_show_toolbar.set_active(True) window = self.manager.window window.set_transient_for(self.application.get_active_window()) window.set_title(self.graph_title) return window @property def markersize_scale(self): bbox = self.axes[0].get_window_extent().transformed(self.figure.dpi_scale_trans.inverted()) return bbox.width * self.figure.dpi * 0.01 def mpl_signal_canvas_button_pressed(self, event): if event.button != 3: return self.popup_menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time()) return True def signal_activate_popup_menu_export(self, action): dialog = gui_utilities.FileChooser('Export Graph', self.application.get_active_window()) file_name = self.config['campaign_name'] + '.png' response = dialog.run_quick_save(file_name) dialog.destroy() if not response: return destination_file = response['target_path'] self.figure.savefig(destination_file, format='png') def signal_toggled_popup_menu_show_toolbar(self, widget): if widget.get_property('active'): self.navigation_toolbar.show() else: self.navigation_toolbar.hide() def load_graph(self): """Load the graph information via :py:meth:`.refresh`.""" self.refresh() def refresh(self, info_cache=None, stop_event=None): """ Refresh the graph data by retrieving the information from the remote server. :param dict info_cache: An optional cache of data tables. :param stop_event: An optional object indicating that the operation should stop. :type stop_event: :py:class:`threading.Event` :return: A dictionary of cached tables from the server. :rtype: dict """ info_cache = (info_cache or {}) if not self.rpc: return info_cache for table in self.table_subscriptions: if stop_event and stop_event.is_set(): return info_cache if not table in info_cache: info_cache[table] = tuple(self.rpc.remote_table(table, query_filter={'campaign_id': self.config['campaign_id']})) for ax in self.axes: ax.clear() if self._legend is not None: self._legend.remove() self._legend = None self._load_graph(info_cache) self.figure.suptitle( self.graph_title, color=self.get_color('fg', ColorHexCode.BLACK), size=14, weight='bold', y=0.97 ) self.canvas.draw() return info_cache
class CampaignGraph(object): title = 'Unknown' _graph_id = None table_subscriptions = [] def __init__(self, config, parent, size_request=None): self.config = config self.parent = parent self.figure, ax = pyplot.subplots() self.axes = self.figure.get_axes() self.canvas = FigureCanvas(self.figure) self.manager = None if size_request: self.canvas.set_size_request(*size_request) self.canvas.mpl_connect('button_press_event', self.mpl_signal_canvas_button_pressed) self.canvas.show() self.navigation_toolbar = NavigationToolbar(self.canvas, self.parent) self.navigation_toolbar.hide() self.popup_menu = Gtk.Menu.new() menu_item = Gtk.MenuItem.new_with_label('Export') menu_item.connect('activate', self.signal_activate_popup_menu_export) self.popup_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('Refresh') menu_item.connect('activate', lambda action: self.refresh()) self.popup_menu.append(menu_item) menu_item = Gtk.CheckMenuItem.new_with_label('Show Toolbar') menu_item.connect('toggled', self.signal_toggled_popup_menu_show_toolbar) self.popup_menu.append(menu_item) self.popup_menu.show_all() @classmethod def get_graph_id(klass): return klass._graph_id def make_window(self): if self.manager == None: self.manager = FigureManager(self.canvas, 0) window = self.manager.window window.set_transient_for(self.parent) window.set_title(self.title) return window def mpl_signal_canvas_button_pressed(self, event): if event.button != 3: return pos_func = lambda m, d: (event.x, event.y, True) self.popup_menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time()) return True def signal_activate_popup_menu_export(self, action): dialog = gui_utilities.UtilityFileChooser('Export Graph', self.parent) file_name = self.config['campaign_name'] + '.png' response = dialog.run_quick_save(file_name) dialog.destroy() if not response: return destination_file = response['target_filename'] self.figure.savefig(destination_file, format='png') def signal_toggled_popup_menu_show_toolbar(self, widget): if widget.get_property('active'): self.navigation_toolbar.show() else: self.navigation_toolbar.hide() def load_graph(self): self.refresh() def refresh(self, info_cache=None): info_cache = (info_cache or {}) if not self.parent.rpc: return info_cache for table in self.table_subscriptions: if not table in info_cache: info_cache[table] = list(self.parent.rpc.remote_table('campaign/' + table, self.config['campaign_id'])) map(lambda ax: ax.clear(), self.axes) self._load_graph(info_cache) self.canvas.draw() return info_cache
class CampaignGraph(object): """ A basic graph provider for using :py:mod:`matplotlib` to create graph representations of campaign data. This class is meant to be subclassed by real providers. """ name = 'Unknown' """The name of the graph provider.""" name_human = 'Unknown' """The human readable name of the graph provider used for UI identification.""" graph_title = 'Unknown' """The title that will be given to the graph.""" table_subscriptions = [] """A list of tables from which information is needed to produce the graph.""" is_available = True def __init__(self, config, parent, size_request=None): """ :param dict config: The King Phisher client configuration. :param parent: The parent window for this object. :type parent: :py:class:`Gtk.Window` :param tuple size_request: The size to set for the canvas. """ self.config = config """A reference to the King Phisher client configuration.""" self.parent = parent """The parent :py:class:`Gtk.Window` instance.""" self.figure, _ = pyplot.subplots() self.axes = self.figure.get_axes() self.canvas = FigureCanvas(self.figure) self.manager = None if size_request: self.canvas.set_size_request(*size_request) self.canvas.mpl_connect('button_press_event', self.mpl_signal_canvas_button_pressed) self.canvas.show() self.navigation_toolbar = NavigationToolbar(self.canvas, self.parent) self.popup_menu = Gtk.Menu.new() menu_item = Gtk.MenuItem.new_with_label('Export') menu_item.connect('activate', self.signal_activate_popup_menu_export) self.popup_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('Refresh') menu_item.connect('activate', lambda action: self.refresh()) self.popup_menu.append(menu_item) menu_item = Gtk.CheckMenuItem.new_with_label('Show Toolbar') menu_item.connect('toggled', self.signal_toggled_popup_menu_show_toolbar) self._menu_item_show_toolbar = menu_item self.popup_menu.append(menu_item) self.popup_menu.show_all() self.navigation_toolbar.hide() def _load_graph(self, info_cache): raise NotImplementedError() def _graph_bar_set_yparams(self, top_lim): min_value = top_lim + (top_lim * 0.075) if min_value <= 25: scale = 5 else: scale = scale = 10 ** (len(str(int(min_value))) - 1) inc_scale = scale while scale <= min_value: scale += inc_scale top_lim = scale ax = self.axes[0] yticks = set((round(top_lim * 0.5), top_lim)) ax.set_yticks(tuple(yticks)) ax.set_ylim(top=top_lim) return def _graph_null_pie(self, title): ax = self.axes[0] ax.pie((100,), labels=(title,), colors=(MPL_COLOR_NULL,), autopct='%1.0f%%', shadow=True, startangle=90) ax.axis('equal') return def add_legend_patch(self, legend_rows, fontsize=None): handles = [] if not fontsize: scale = self.markersize_scale if scale < 5: fontsize = 'xx-small' elif scale < 7: fontsize = 'x-small' elif scale < 9: fontsize = 'small' else: fontsize = 'medium' for row in legend_rows: handles.append(patches.Patch(color=row[0], label=row[1])) self.axes[0].legend(handles=handles, fontsize=fontsize, loc='lower right') def graph_bar(self, bars, color=None, xticklabels=None, ylabel=None): """ Create a standard bar graph with better defaults for the standard use cases. :param list bars: The values of the bars to graph. :param color: The color of the bars on the graph. :type color: list, str :param list xticklabels: The labels to use on the x-axis. :param str ylabel: The label to give to the y-axis. :return: The bars created using :py:mod:`matplotlib` :rtype: `matplotlib.container.BarContainer` """ color = color or MPL_COLOR_NULL width = 0.25 ax = self.axes[0] self._graph_bar_set_yparams(max(bars) if bars else 0) bars = ax.bar(range(len(bars)), bars, width, color=color) ax.set_xticks([float(x) + (width / 2) for x in range(len(bars))]) if xticklabels: ax.set_xticklabels(xticklabels, rotation=30) for col in bars: height = col.get_height() ax.text(col.get_x() + col.get_width() / 2.0, height, "{0:,}".format(height), ha='center', va='bottom') if ylabel: ax.set_ylabel(ylabel) self.figure.subplots_adjust(bottom=0.25) return bars def make_window(self): """ Create a window from the figure manager. :return: The graph in a new, dedicated window. :rtype: :py:class:`Gtk.Window` """ if self.manager == None: self.manager = FigureManager(self.canvas, 0) self.navigation_toolbar.destroy() self.navigation_toolbar = self.manager.toolbar self._menu_item_show_toolbar.set_active(True) window = self.manager.window window.set_transient_for(self.parent) window.set_title(self.graph_title) return window @property def markersize_scale(self): bbox = self.axes[0].get_window_extent().transformed(self.figure.dpi_scale_trans.inverted()) return max(bbox.width, bbox.width) * self.figure.dpi * 0.01 def mpl_signal_canvas_button_pressed(self, event): if event.button != 3: return self.popup_menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time()) return True def signal_activate_popup_menu_export(self, action): dialog = gui_utilities.FileChooser('Export Graph', self.parent) file_name = self.config['campaign_name'] + '.png' response = dialog.run_quick_save(file_name) dialog.destroy() if not response: return destination_file = response['target_path'] self.figure.savefig(destination_file, format='png') def signal_toggled_popup_menu_show_toolbar(self, widget): if widget.get_property('active'): self.navigation_toolbar.show() else: self.navigation_toolbar.hide() def load_graph(self): """Load the graph information via :py:meth:`.refresh`.""" self.refresh() def refresh(self, info_cache=None, stop_event=None): """ Refresh the graph data by retrieving the information from the remote server. :param dict info_cache: An optional cache of data tables. :param stop_event: An optional object indicating that the operation should stop. :type stop_event: :py:class:`threading.Event` :return: A dictionary of cached tables from the server. :rtype: dict """ info_cache = (info_cache or {}) if not self.parent.rpc: return info_cache for table in self.table_subscriptions: if stop_event and stop_event.is_set(): return info_cache if not table in info_cache: info_cache[table] = tuple(self.parent.rpc.remote_table('campaign/' + table, self.config['campaign_id'])) for ax in self.axes: ax.clear() self._load_graph(info_cache) self.axes[0].set_title(self.graph_title, y=1.03) self.canvas.draw() return info_cache
class CampaignGraph(object): """ A basic graph provider for using :py:mod:`matplotlib` to create graph representations of campaign data. This class is meant to be subclassed by real providers. """ title = 'Unknown' """The title of the graph.""" _graph_id = None table_subscriptions = [] """A list of tables from which information is needed to produce the graph.""" def __init__(self, config, parent, size_request=None): """ :param dict config: The King Phisher client configuration. :param parent: The parent window for this object. :type parent: :py:class:`Gtk.Window` :param tuple size_request: The size to set for the canvas. """ self.config = config """A reference to the King Phisher client configuration.""" self.parent = parent """The parent :py:class:`Gtk.Window` instance.""" self.figure, ax = pyplot.subplots() self.axes = self.figure.get_axes() self.canvas = FigureCanvas(self.figure) self.manager = None if size_request: self.canvas.set_size_request(*size_request) self.canvas.mpl_connect('button_press_event', self.mpl_signal_canvas_button_pressed) self.canvas.show() self.navigation_toolbar = NavigationToolbar(self.canvas, self.parent) self.navigation_toolbar.hide() self.popup_menu = Gtk.Menu.new() menu_item = Gtk.MenuItem.new_with_label('Export') menu_item.connect('activate', self.signal_activate_popup_menu_export) self.popup_menu.append(menu_item) menu_item = Gtk.MenuItem.new_with_label('Refresh') menu_item.connect('activate', lambda action: self.refresh()) self.popup_menu.append(menu_item) menu_item = Gtk.CheckMenuItem.new_with_label('Show Toolbar') menu_item.connect('toggled', self.signal_toggled_popup_menu_show_toolbar) self.popup_menu.append(menu_item) self.popup_menu.show_all() @classmethod def get_graph_id(klass): """ The graph id of an exported :py:class:`.CampaignGraph`. :param klass: The class to return the graph id of. :type klass: :py:class:`.CampaignGraph` :return: The id of the graph. :rtype: int """ return klass._graph_id def make_window(self): """ Create a window from the figure manager. :return: The graph in a new, dedicated window. :rtype: :py:class:`Gtk.Window` """ if self.manager == None: self.manager = FigureManager(self.canvas, 0) window = self.manager.window window.set_transient_for(self.parent) window.set_title(self.title) return window def mpl_signal_canvas_button_pressed(self, event): if event.button != 3: return self.popup_menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time()) return True def signal_activate_popup_menu_export(self, action): dialog = gui_utilities.UtilityFileChooser('Export Graph', self.parent) file_name = self.config['campaign_name'] + '.png' response = dialog.run_quick_save(file_name) dialog.destroy() if not response: return destination_file = response['target_path'] self.figure.savefig(destination_file, format='png') def signal_toggled_popup_menu_show_toolbar(self, widget): if widget.get_property('active'): self.navigation_toolbar.show() else: self.navigation_toolbar.hide() def load_graph(self): """Load the graph information via :py:meth:`.refresh`.""" self.refresh() def refresh(self, info_cache=None, stop_event=None): """ Refresh the graph data by retrieving the information from the remote server. :param dict info_cache: An optional cache of data tables. :param stop_event: An optional object indicating that the operation should stop. :type stop_event: :py:class:`threading.Event` :return: A dictionary of cached tables from the server. :rtype: dict """ info_cache = (info_cache or {}) if not self.parent.rpc: return info_cache for table in self.table_subscriptions: if stop_event and stop_event.is_set(): return info_cache if not table in info_cache: info_cache[table] = list( self.parent.rpc.remote_table('campaign/' + table, self.config['campaign_id'])) map(lambda ax: ax.clear(), self.axes) self._load_graph(info_cache) self.canvas.draw() return info_cache