From faff57b46561ad2d1ff057461c747c2331dfffb2 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 5 Oct 2017 16:07:51 -0400 Subject: [PATCH 01/38] Add the new send-message signal --- docs/source/client/gobject_signals.rst | 12 ++++++++++++ king_phisher/client/mailer.py | 1 + king_phisher/client/tabs/mail.py | 1 + king_phisher/version.py | 2 +- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/source/client/gobject_signals.rst b/docs/source/client/gobject_signals.rst index d7cb9534..d9893c60 100644 --- a/docs/source/client/gobject_signals.rst +++ b/docs/source/client/gobject_signals.rst @@ -205,6 +205,18 @@ The following are the signals for the :signal flags: ``SIGNAL_RUN_FIRST`` +.. py:function:: send-message(target, message) + + This signal is emitted when the message for a target has been loaded and + constructed. Subscribers to this signal may use it as an oppertunity to + modify the message object prior to it being sent. + + :signal flags: ``SIGNAL_RUN_FIRST`` + :param target: The target for the message. + :type target: :py:class:`~king_phisher.client.mailer.MessageTarget` + :param message: The message about to be sent to the target. + :type message: :py:class:`~king_phisher.client.mailer.TopMIMEMultipart` + .. py:function:: send-precheck() This signal is emitted when the user is about to start sending phishing diff --git a/king_phisher/client/mailer.py b/king_phisher/client/mailer.py index ff29ef23..70b7a124 100644 --- a/king_phisher/client/mailer.py +++ b/king_phisher/client/mailer.py @@ -739,6 +739,7 @@ def _send_messages(self): mailer_tab.emit('send-target', target) attachments = self.get_mime_attachments() msg = getattr(self, 'create_' + self.config['mailer.message_type'])(target, attachments) + mailer_tab.emit('send-message', target, msg) if not self._try_send_message(target.email_address, msg): break diff --git a/king_phisher/client/tabs/mail.py b/king_phisher/client/tabs/mail.py index 9dbb75d8..750735d9 100644 --- a/king_phisher/client/tabs/mail.py +++ b/king_phisher/client/tabs/mail.py @@ -1022,6 +1022,7 @@ class MailSenderTab(_GObject_GObject): 'message-data-import': (GObject.SIGNAL_ACTION | GObject.SIGNAL_RUN_LAST, bool, (str, str)), 'send-finished': (GObject.SIGNAL_RUN_FIRST, None, ()), 'send-precheck': (GObject.SIGNAL_RUN_LAST, object, (), gui_utilities.gobject_signal_accumulator(test=lambda r, a: r)), + 'send-message': (GObject.SIGNAL_RUN_FIRST, None, (object, object)), 'send-target': (GObject.SIGNAL_RUN_FIRST, None, (object,)) } def __init__(self, parent, application): diff --git a/king_phisher/version.py b/king_phisher/version.py index 0451d4f1..900e7cfd 100644 --- a/king_phisher/version.py +++ b/king_phisher/version.py @@ -67,7 +67,7 @@ def get_revision(): version_info = collections.namedtuple('version_info', ('major', 'minor', 'micro'))(1, 9, 0) """A tuple representing the version information in the format ('major', 'minor', 'micro')""" -version_label = 'beta4' +version_label = 'beta5' """A version label such as alpha or beta.""" version = "{0}.{1}.{2}".format(version_info.major, version_info.minor, version_info.micro) From 05577e64451b8a4eb30707e4628d6901fbb2c069 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 6 Oct 2017 13:37:56 -0400 Subject: [PATCH 02/38] Add a utility gobject_set_value function --- .../king_phisher/client/gui_utilities.rst | 2 ++ king_phisher/client/gui_utilities.py | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/source/king_phisher/client/gui_utilities.rst b/docs/source/king_phisher/client/gui_utilities.rst index 65954a5c..7c220969 100644 --- a/docs/source/king_phisher/client/gui_utilities.rst +++ b/docs/source/king_phisher/client/gui_utilities.rst @@ -22,6 +22,8 @@ Functions .. autofunction:: king_phisher.client.gui_utilities.gobject_get_value +.. autofunction:: king_phisher.client.gui_utilities.gobject_set_value + .. autofunction:: king_phisher.client.gui_utilities.gobject_signal_accumulator .. autofunction:: king_phisher.client.gui_utilities.gobject_signal_blocked diff --git a/king_phisher/client/gui_utilities.py b/king_phisher/client/gui_utilities.py index 8c764da0..a54549a9 100644 --- a/king_phisher/client/gui_utilities.py +++ b/king_phisher/client/gui_utilities.py @@ -116,6 +116,7 @@ def glib_idle_add_wait(function, *args, **kwargs): """ gsource_completed = threading.Event() results = [] + @functools.wraps(function) def wrapper(): results.append(function(*args, **kwargs)) @@ -127,9 +128,9 @@ def wrapper(): def gobject_get_value(gobject, gtype=None): """ - Retreive the value of a GObject widget. Only objects with value - retrieving functions present in the :py:data:`.GOBJECT_PROPERTY_MAP` - can be processed by this function. + Retrieve the value of a GObject widget. Only objects with corresponding + entries present in the :py:data:`.GOBJECT_PROPERTY_MAP` can be processed by + this function. :param gobject: The object to retrieve the value for. :type gobject: :py:class:`GObject.Object` @@ -148,6 +149,26 @@ def gobject_get_value(gobject, gtype=None): value = gobject.get_property(GOBJECT_PROPERTY_MAP[gtype]) return value +def gobject_set_value(gobject, value, gtype=None): + """ + Set the value of a GObject widget. Only objects with corresponding entries + present in the :py:data:`.GOBJECT_PROPERTY_MAP` can be processed by this + function. + + :param gobject: The object to set the value for. + :type gobject: :py:class:`GObject.Object` + :param value: The value to set for the object. + :param str gtype: An explicit type to treat *gobject* as. + """ + gtype = (gtype or gobject.__class__.__name__) + gtype = gtype.lower() + if gtype not in GOBJECT_PROPERTY_MAP: + raise ValueError('unsupported gtype: ' + gtype) + if isinstance(GOBJECT_PROPERTY_MAP[gtype], (list, tuple)): + GOBJECT_PROPERTY_MAP[gtype][0](gobject, value) + else: + gobject.set_property(GOBJECT_PROPERTY_MAP[gtype], value) + @contextlib.contextmanager def gobject_signal_blocked(gobject, signal_name): """ @@ -179,6 +200,7 @@ def gobject_signal_accumulator(test=None): """ if test is None: test = lambda retval, accumulated: True + def _accumulator(_, accumulated, retval): if accumulated is None: accumulated = [] From 4a5db85ddef344f5e135ce646205f231f939751c Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Mon, 9 Oct 2017 16:03:27 -0400 Subject: [PATCH 03/38] Fixed xlsx export issue #227 --- king_phisher/client/export.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/king_phisher/client/export.py b/king_phisher/client/export.py index 5bd26482..70b0b48a 100644 --- a/king_phisher/client/export.py +++ b/king_phisher/client/export.py @@ -429,6 +429,14 @@ def liststore_to_xlsx_worksheet(store, worksheet, columns, title_format, xlsx_op start_row = 2 worksheet.merge_range(0, 0, 0, len(column_names) - 1, xlsx_options.title, title_format) row_count = liststore_export(store, columns, _xlsx_write, (worksheet,), row_offset=start_row, write_columns=False) + + if not row_count: + column_ = 0 + for column_name in column_names: + worksheet.write(start_row, column_, column_name) + column_ += 1 + return row_count + options = { 'columns': list({'header': column_name} for column_name in column_names), 'style': 'Table Style Medium 1' From 3d7b6b440a9b981110e63cb73bba6dfff9d637f6 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 9 Oct 2017 16:14:20 -0400 Subject: [PATCH 04/38] Add a MultilineEntry widget --- data/client/king_phisher/style/theme.v2.css | 36 +++++++++++++++++-- data/client/king_phisher/style/theme.v2.scss | 38 ++++++++++++++++++-- king_phisher/client/dialogs/configuration.py | 1 + king_phisher/client/plugins.py | 20 ++++++++--- king_phisher/client/widget/extras.py | 32 +++++++++++++++++ 5 files changed, 118 insertions(+), 9 deletions(-) diff --git a/data/client/king_phisher/style/theme.v2.css b/data/client/king_phisher/style/theme.v2.css index ce260fa5..0acad297 100644 --- a/data/client/king_phisher/style/theme.v2.css +++ b/data/client/king_phisher/style/theme.v2.css @@ -97,6 +97,33 @@ background-image: linear-gradient(to bottom, shade(#00203a, 1.25), shade(#00203a, 1.5)); font-weight: bold; } +.multilineentry { + background-color: alpha(white, 0.8); + border: 1px solid; + border-color: shade(#00203a, 1.2); + border-radius: 3px; } + .multilineentry:disabled { + border-color: white; } + .multilineentry:disabled { + background-color: alpha(darkgray, 0.8); } + .multilineentry > textview.view { + background-color: transparent; + background-image: none; + border-radius: 2px; + padding-bottom: 7px; + padding-left: 8px; + padding-right: 8px; + padding-top: 6px; } + .multilineentry > textview.view text { + background: transparent; + color: #00203a; + min-height: 32px; } + .multilineentry > textview.view text:disabled { + color: white; } + .multilineentry > textview.view text > selection { + background-color: #5d84a8; + color: white; } + .titlebar { background-color: #5d84a8; background-image: none; @@ -174,14 +201,14 @@ entry { border-radius: 3px; background-color: alpha(white, 0.8); background-image: none; - border-radius: 3px; - color: #00203a; } + color: #00203a; + min-height: 32px; } entry:disabled { border-color: white; } entry:disabled { background-color: alpha(darkgray, 0.8); color: white; } - entry:selected { + entry > selection { background-color: #5d84a8; color: white; } @@ -361,6 +388,9 @@ scrollbar { scrollbar.right { border-left: 1px solid shade(#00203a, 1.2); } +scrolledwindow { + min-height: 32px; } + spinbutton { background-color: transparent; background-image: none; diff --git a/data/client/king_phisher/style/theme.v2.scss b/data/client/king_phisher/style/theme.v2.scss index 9d09c96f..fd4e0f53 100644 --- a/data/client/king_phisher/style/theme.v2.scss +++ b/data/client/king_phisher/style/theme.v2.scss @@ -75,6 +75,36 @@ } } + +.multilineentry { + background-color: gtkalpha(white, 0.8); + @include bo-opaque(); + &:disabled { + background-color: gtkalpha(darkgray, 0.8); + } + > textview.view { + background-color: transparent; + background-image: none; + border-radius: 2px; + padding-bottom: 7px; + padding-left: 8px; + padding-right: 8px; + padding-top: 6px; + text { + background: transparent; + color: $theme_color_0; + min-height: 32px; + &:disabled { + color: white; + } + > selection { + background-color: $theme_color_1; + color: white; + } + } + } +} + .titlebar { background-color: mix($theme_color_0, $theme_color_1, 0.4); background-image: none; @@ -149,13 +179,13 @@ entry { @include bo-opaque(); background-color: gtkalpha(white, 0.8); background-image: none; - border-radius: 3px; color: $theme_color_0; + min-height: 32px; &:disabled { background-color: gtkalpha(darkgray, 0.8); color: white; } - &:selected { + > selection { background-color: $theme_color_1; color: white; } @@ -341,6 +371,10 @@ scrollbar { &.right { border-left: 1px solid $theme_color_bg; } } +scrolledwindow { + min-height: 32px; +} + spinbutton { background-color: transparent; background-image: none; diff --git a/king_phisher/client/dialogs/configuration.py b/king_phisher/client/dialogs/configuration.py index 02ff9457..c24f27fd 100644 --- a/king_phisher/client/dialogs/configuration.py +++ b/king_phisher/client/dialogs/configuration.py @@ -67,6 +67,7 @@ def __init__(self, application, plugin_klass): else: grid.set_property('margin-start', 12) grid.set_property('column-spacing', 3) + grid.set_property('hexpand', True) grid.set_property('row-spacing', 3) grid.insert_column(0) grid.insert_column(0) diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 23fe05f4..51f076e6 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -169,17 +169,29 @@ def set_widget_value(self, _, value): self.adjustment.set_value(int(round(value))) class ClientOptionString(ClientOptionMixin, plugins.OptionString): + def __init__(self, name, *args, **kwargs): + self.multiline = bool(kwargs.pop('multiline', False)) + super(ClientOptionString, self).__init__(name, *args, **kwargs) + def get_widget(self, _, value): - widget = Gtk.Entry() - widget.set_hexpand(True) + if self.multiline: + #scrolled_window = Gtk.ScrolledWindow() + textview = extras.MultilineEntry() + textview.set_property('hexpand', True) + #scrolled_window.add(textview) + #scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + widget = textview + else: + widget = Gtk.Entry() + widget.set_hexpand(True) self.set_widget_value(widget, value) return widget def get_widget_value(self, widget): - return widget.get_text() + return widget.get_property('text') def set_widget_value(self, widget, value): - widget.set_text((value if value else '')) + widget.set_property('text', value) return widget # extended option types diff --git a/king_phisher/client/widget/extras.py b/king_phisher/client/widget/extras.py index c59ba899..c6cbdda2 100644 --- a/king_phisher/client/widget/extras.py +++ b/king_phisher/client/widget/extras.py @@ -53,10 +53,12 @@ if its.mocked: _Gtk_CellRendererText = type('Gtk.CellRendererText', (object,), {'__module__': ''}) _Gtk_FileChooserDialog = type('Gtk.FileChooserDialog', (object,), {'__module__': ''}) + _Gtk_Frame = type('Gtk.Frame', (object,), {'__module__': ''}) _WebKitX_WebView = type('WebKitX.WebView', (object,), {'__module__': ''}) else: _Gtk_CellRendererText = Gtk.CellRendererText _Gtk_FileChooserDialog = Gtk.FileChooserDialog + _Gtk_Frame = Gtk.Frame _WebKitX_WebView = WebKitX.WebView class CellRendererBytes(_Gtk_CellRendererText): @@ -167,6 +169,36 @@ def run_quick_select_directory(self): target_path = self.get_filename() return {'target_uri': target_uri, 'target_path': target_path} +class MultilineEntry(_Gtk_Frame): + __gproperties__ = { + 'text': (str, 'text', 'The contents of the entry.', '', GObject.ParamFlags.READWRITE), + 'text-length': (int, 'text-length', 'The length of the text in the GtkEntry.', 0, 0xffff, 0, GObject.ParamFlags.READABLE) + } + __gtype_name__ = 'MultilineEntry' + def __init__(self, *args, **kwargs): + Gtk.Frame.__init__(self, *args, **kwargs) + self.get_style_context().add_class('multilineentry') + textview = Gtk.TextView() + self.add(textview) + + def do_get_property(self, prop): + textview = self.get_child() + if prop.name == 'text': + buffer = textview.get_buffer() + return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False) + elif prop.name == 'text-length': + return 0 + raise AttributeError('unknown property: ' + prop.name) + + def do_set_property(self, prop, value): + textview = self.get_child() + if prop.name == 'text': + textview.get_buffer().set_text(value) + elif prop.name == 'text-length': + raise ValueError('read-only property: ' + prop.name) + else: + raise AttributeError('unknown property: ' + prop.name) + class WebKitHTMLView(_WebKitX_WebView): """ A WebView widget with additional convenience methods for rendering simple From c963a010be399e97bd6bdd9c1c75700ba19db723 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 9 Oct 2017 16:50:47 -0400 Subject: [PATCH 05/38] Document the new MultilineEntry widget --- docs/source/king_phisher/client/widget/extras.rst | 6 ++++++ king_phisher/client/plugins.py | 15 +++++++++++---- king_phisher/client/widget/extras.py | 4 ++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/source/king_phisher/client/widget/extras.rst b/docs/source/king_phisher/client/widget/extras.rst index 354e14ef..fff95988 100644 --- a/docs/source/king_phisher/client/widget/extras.rst +++ b/docs/source/king_phisher/client/widget/extras.rst @@ -17,6 +17,12 @@ Classes :special-members: __init__ :undoc-members: +.. autoclass:: king_phisher.client.widget.extras.MultilineEntry + :show-inheritance: + :members: + :special-members: __init__ + :undoc-members: + .. autoclass:: king_phisher.client.widget.extras.WebKitHTMLView :show-inheritance: :members: diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 51f076e6..7d3980e7 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -175,12 +175,13 @@ def __init__(self, name, *args, **kwargs): def get_widget(self, _, value): if self.multiline: - #scrolled_window = Gtk.ScrolledWindow() + scrolled_window = Gtk.ScrolledWindow() textview = extras.MultilineEntry() textview.set_property('hexpand', True) - #scrolled_window.add(textview) - #scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - widget = textview + scrolled_window.add(textview) + scrolled_window.set_property('height-request', 50) + scrolled_window.set_property('vscrollbar-policy', Gtk.PolicyType.ALWAYS) + widget = scrolled_window else: widget = Gtk.Entry() widget.set_hexpand(True) @@ -188,9 +189,15 @@ def get_widget(self, _, value): return widget def get_widget_value(self, widget): + if self.multiline: + widget = widget.get_child().get_child() return widget.get_property('text') def set_widget_value(self, widget, value): + if value is None: + value = '' + if self.multiline: + widget = widget.get_child().get_child() widget.set_property('text', value) return widget diff --git a/king_phisher/client/widget/extras.py b/king_phisher/client/widget/extras.py index c6cbdda2..c2d38ee3 100644 --- a/king_phisher/client/widget/extras.py +++ b/king_phisher/client/widget/extras.py @@ -170,6 +170,10 @@ def run_quick_select_directory(self): return {'target_uri': target_uri, 'target_path': target_path} class MultilineEntry(_Gtk_Frame): + """ + A custom entry widget which can be styled to look like + :py:class:`Gtk.Entry` but accepts multiple lines of input. + """ __gproperties__ = { 'text': (str, 'text', 'The contents of the entry.', '', GObject.ParamFlags.READWRITE), 'text-length': (int, 'text-length', 'The length of the text in the GtkEntry.', 0, 0xffff, 0, GObject.ParamFlags.READABLE) From b405b6cadd788cb99d4f338aba9e26361725ac1a Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 10 Oct 2017 10:27:09 -0400 Subject: [PATCH 06/38] Add client plugin API features --- king_phisher/client/plugins.py | 70 ++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 7d3980e7..b57bd1a2 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -37,9 +37,11 @@ from king_phisher import plugins from king_phisher.client import gui_utilities +from king_phisher.client import mailer from king_phisher.client.widget import extras from gi.repository import Gtk +import jinja2.exceptions def _split_menu_path(menu_path): menu_path = [path_item.strip() for path_item in menu_path.split('>')] @@ -113,13 +115,16 @@ def set_widget_value(self, widget, value): return widget class ClientOptionEnum(ClientOptionMixin, plugins.OptionEnum): - """ - :param str name: The name of this option. - :param str description: The description of this option. - :param tuple choices: The supported values for this option. - :param default: The default value of this option. - :param str display_name: The name to display in the UI to the user for this option - """ + def __init__(self, name, *args, **kwargs): + """ + :param str name: The name of this option. + :param str description: The description of this option. + :param tuple choices: The supported values for this option. + :param default: The default value of this option. + :param str display_name: The name to display in the UI to the user for this option + """ + super(ClientOptionEnum, self).__init__(name, *args, **kwargs) + def get_widget(self, _, value): widget = Gtk.ComboBoxText() widget.set_hexpand(True) @@ -170,6 +175,16 @@ def set_widget_value(self, _, value): class ClientOptionString(ClientOptionMixin, plugins.OptionString): def __init__(self, name, *args, **kwargs): + """ + .. versionchanged:: 1.9.0b5 + Added the *multiline* option. + + :param str name: The name of this option. + :param str description: The description of this option. + :param default: The default value of this option. + :param str display_name: The name to display in the UI to the user for this option. + :param bool multiline: Whether or not this option allows multiple lines of input. + """ self.multiline = bool(kwargs.pop('multiline', False)) super(ClientOptionString, self).__init__(name, *args, **kwargs) @@ -179,7 +194,7 @@ def get_widget(self, _, value): textview = extras.MultilineEntry() textview.set_property('hexpand', True) scrolled_window.add(textview) - scrolled_window.set_property('height-request', 50) + scrolled_window.set_property('height-request', 60) scrolled_window.set_property('vscrollbar-policy', Gtk.PolicyType.ALWAYS) widget = scrolled_window else: @@ -365,6 +380,45 @@ def add_submenu(self, menu_path): menu_item.set_submenu(Gtk.Menu.new()) return self._insert_menu_item(menu_path, menu_item) + def render_template_string(self, template_string, target=None, description='string', log_to_mailer=True): + """ + Render the specified *template_string* in the message environment. If + an error occurs during the rendering process, a message will be logged + and ``None`` will be returned. If *log_to_mailer* is set to ``True`` + then a message will also be displayed in the message send tab of the + client. + + .. versionadded:: 1.9.0b5 + + :param str template_string: The string to render as a template. + :param target: An optional target to pass to the rendering environment. + :type target: :py:class:`~king_phisher.client.mailer.MessageTarget` + :param str description: A keyword to use to identify the template string in error messages. + :param bool log_to_mailer: Whether or not to log to the message send tab as well. + :return: The rendered string or ``None`` if an error occurred. + :rtype: str + """ + mailer_tab = self.application.main_tabs['mailer'] + text_insert = mailer_tab.tabs['send_messages'].text_insert + try: + template_string = mailer.render_message_template(template_string, self.application.config, target=target) + except jinja2.exceptions.TemplateSyntaxError as error: + self.logger.error("jinja2 syntax error ({0}) in {1}: {2}".format(error.message, description, template_string)) + if log_to_mailer: + text_insert("Jinja2 syntax error ({0}) in {1}: {2}\n".format(error.message, description, template_string)) + return None + except jinja2.exceptions.UndefinedError as error: + self.logger.error("jinj2 undefined error ({0}) in {1}: {2}".format(error.message, description, template_string)) + if log_to_mailer: + text_insert("Jinja2 undefined error ({0}) in {1}: {2}".format(error.message, description, template_string)) + return None + except ValueError as error: + self.logger.error("value error ({0}) in {1}: {2}".format(error, description, template_string)) + if log_to_mailer: + text_insert("Value error ({0}) in {1}: {2}\n".format(error, description, template_string)) + return None + return template_string + def signal_connect(self, name, handler, gobject=None, after=False): """ Connect *handler* to a signal by *name* to an arbitrary GObject. Signals From ca2dd29733b2d8cd23c73d1f15b1edc09ee73540 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 10 Oct 2017 12:52:03 -0400 Subject: [PATCH 07/38] Backport the multilineentry style css --- data/client/king_phisher/style/theme.v1.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/data/client/king_phisher/style/theme.v1.css b/data/client/king_phisher/style/theme.v1.css index b72c8418..801c0556 100644 --- a/data/client/king_phisher/style/theme.v1.css +++ b/data/client/king_phisher/style/theme.v1.css @@ -433,3 +433,19 @@ GtkWindow:hover { .background-remove { background-image: none; } + +.multilineentry { + background-color: alpha(white, 0.8); + background-image: none; + border: 0px; + border-radius: 3px; + padding: 5px 5px 6px; } + .multilineentry:insensitive { + background-color: alpha(darkgray, 0.8); } + .multilineentry > GtkTextView { + background-color: transparent; + background-image: none; + color: @theme_color_0; } + .multilineentry > GtkTextView:selected { + background-color: @theme_color_1; + color: white; } From eea79d9c41b29d9ae63ef6141e60144d536882bf Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Thu, 21 Sep 2017 10:35:46 -0400 Subject: [PATCH 08/38] Updated plugin GUI for store --- .../king_phisher/king-phisher-client.ui | 460 ++++++++++-------- king_phisher/client/windows/plugin_manager.py | 54 +- 2 files changed, 290 insertions(+), 224 deletions(-) diff --git a/data/client/king_phisher/king-phisher-client.ui b/data/client/king_phisher/king-phisher-client.ui index c30d8ebc..9c49421e 100644 --- a/data/client/king_phisher/king-phisher-client.ui +++ b/data/client/king_phisher/king-phisher-client.ui @@ -2719,274 +2719,306 @@ Spencer McIntyre - 550 - 300 + 700 + 400 False 5 Plugin Manager center-on-parent True - + True - True + False vertical - - + True True - in + vertical + - + True True - True - - - + in + + + True + True + True + + + + + + + True + False + - - - True - False - - - - - True - True - - - 125 + True True - True - 3 - in + - + + 125 True - False + True + True + 3 + in - + True False - + True False - 3 - 3 - True - - - True - False - False - - - - - - - 0 - 0 - 4 - - - - - True - False - False - end - start - Authors: - - - - - - 0 - 2 - - - - - True - False - False - The contributors who have written and provided this plugin. - start - start - - - - - - 1 - 2 - - - - - True - False - False - The version identifier of this plugin. - start - start - - - - - - 3 - 1 - - - + True - False False - end - start - Description: - - - - - - 0 - 3 - - - - - True - False - The description of this plugin and the features it provides. - start - start - True - True - - - - - - 1 - 3 - 3 - - - - - True - False - False - end - start - Version: - - - - - - 2 - 1 - - - - - True - False - Whether or not this plugin is compatible with this environment. - start - start - - - 1 - 1 - - - - - True - True - <a href="">Homepage</a> - True - False - - - - 2 - 2 - 2 - - - - - True - False - + 3 + 3 + True + + + True + False + False + + + + + + + 0 + 0 + 4 + + - + True + False False end start - Compatible: + Authors: - + + + 0 + 2 + + + + + True + False + False + The contributors who have written and provided this plugin. + start + start + + + + + + 1 + 2 + + + + + True + False + False + The version identifier of this plugin. + start + start + + + + + + 3 + 1 + + + + + True + False + False + end + start + Description: + + + + + + 0 + 3 + + + + + True + False + The description of this plugin and the features it provides. + start + start + True + True + + + + + + 1 + 3 + 3 + + + + + True + False + False + end + start + Version: + + + + + + 2 + 1 + + + + + True + False + Whether or not this plugin is compatible with this environment. + start + start + + + 1 + 1 + + + + + True + True + <a href="">Homepage</a> + True + False + + + + 2 + 2 + 2 + + + + + True + False + + + + True + False + end + start + Compatible: + + + + + + + + 0 + 1 + - 0 - 1 + page0 + page0 + + + + + True + True + + + page1 + page1 + 1 - - page0 - page0 - - - - - True - True - - - page1 - page1 - 1 - + + + True + False + Information + + - - - - True - False - Plugin Information - + + False + False + - False - False + True + True + 0 + + + + + True + False + Current Status + 10 + 10 + 10 + 10 + 6 + 6 + vertical + 2 + + + False + True + 1 diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 68441f1e..988241ac 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -36,6 +36,7 @@ from king_phisher import utilities from king_phisher.client import gui_utilities from king_phisher.client.widget import managers +from king_phisher import version from gi.repository import Gdk from gi.repository import Gtk @@ -68,6 +69,7 @@ class PluginManagerWindow(gui_utilities.GladeGObject): ) ) top_gobject = 'window' + def __init__(self, *args, **kwargs): super(PluginManagerWindow, self).__init__(*args, **kwargs) treeview = self.gobjects['treeview_plugins'] @@ -77,15 +79,20 @@ def __init__(self, *args, **kwargs): treeview, cb_refresh=self.load_plugins ) - toggle_renderer = Gtk.CellRendererToggle() - toggle_renderer.connect('toggled', self.signal_renderer_toggled) + toggle_renderer_enable = Gtk.CellRendererToggle() + toggle_renderer_enable.connect('toggled', self.signal_renderer_toggled_enable) + toggle_renderer_install = Gtk.CellRendererToggle() + toggle_renderer_install.connect('toggled', self.signal_renderer_toggled_install) tvm.set_column_titles( - ('Enabled', 'Plugin'), + ['Enabled', 'Installed', 'Type', 'Title', 'Compatible', 'Version'], column_offset=1, - renderers=(toggle_renderer, Gtk.CellRendererText()) + renderers=[toggle_renderer_enable, toggle_renderer_install, Gtk.CellRendererText(), Gtk.CellRendererText(), Gtk.CellRendererText(), Gtk.CellRendererText()] ) - tvm.column_views['Enabled'].set_cell_data_func(toggle_renderer, self._toggle_cell_data_func) - self._model = Gtk.ListStore(str, bool, str) + tvm.column_views['Enabled'].set_cell_data_func(toggle_renderer_enable, self._toggle_cell_data_func) + #tvm.column_views['Installed'].set_cell_data_func(toggle_renderer_enable, self._toggle_cell_data_func) + tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'visible', 7) + tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 7) + self._model = Gtk.TreeStore(str, bool, bool, str, str, str, str, bool) self._model.set_sort_column_id(2, Gtk.SortType.ASCENDING) treeview.set_model(self._model) self.load_plugins() @@ -95,6 +102,9 @@ def __init__(self, *args, **kwargs): menu_item = Gtk.MenuItem.new_with_label('Reload') menu_item.connect('activate', self.signal_popup_menu_activate_reload) self.popup_menu.append(menu_item) + menu_item_reload_all = Gtk.MenuItem.new_with_label('Reload All') + menu_item_reload_all.connect('activate', self.signal_popup_menu_activate_relaod_all) + self.popup_menu.append(menu_item_reload_all) self.popup_menu.show_all() self.window.show() @@ -112,6 +122,9 @@ def _toggle_cell_data_func(self, column, cell, model, tree_iter, _): else: cell.set_property('inconsistent', False) + def signal_popup_menu_activate_relaod_all(self, _): + self.load_plugins() + def load_plugins(self): """ Load the plugins which are available into the treeview to make them @@ -122,11 +135,19 @@ def load_plugins(self): pm = self.application.plugin_manager self._module_errors = {} pm.load_all(on_error=self._on_plugin_load_error) + #(parent (name, bool of check box, installed, type_name column, title column, compatible, version, bool if checkbox is visible)) + catalog = store.append(None, ('catalog', True, None, 'Catalog', 'Manually Installed', None, None, False)) + repo = store.append(catalog, ('repository', True, None, 'Repository', 'Local Paths', None, None, False)) for name, plugin in pm.loaded_plugins.items(): - store.append(( + store.append(repo, ( plugin.name, plugin.name in pm.enabled_plugins, - plugin.title + True, + 'Plugin', + plugin.title, + 'Yes' if version.version >= plugin.req_min_version else 'No', + plugin.version, + True )) for name in self._module_errors.keys(): store.append(( @@ -210,9 +231,11 @@ def signal_popup_menu_activate_reload(self, _): if enabled: pm.enable(name) - def signal_renderer_toggled(self, _, path): + def signal_renderer_toggled_enable(self, _, path): pm = self.application.plugin_manager name = self._model[path][0] # pylint: disable=unsubscriptable-object + if not name or name in ['repo', 'catalog', 'repository']: + return if name in self._module_errors: gui_utilities.show_dialog_error('Can Not Enable Plugin', self.window, 'Can not enable a plugin which failed to load.') return @@ -229,6 +252,9 @@ def signal_renderer_toggled(self, _, path): self._model[path][1] = True # pylint: disable=unsubscriptable-object self.config['plugins.enabled'].append(name) + def signal_renderer_toggled_install(self, _, path): + pass + def signal_treeview_row_activated(self, treeview, path, column): name = self._model[path][0] # pylint: disable=unsubscriptable-object self._set_plugin_info(name) @@ -238,6 +264,11 @@ def _set_plugin_info(self, name): textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() buf.delete(buf.get_start_iter(), buf.get_end_iter()) + if not name or name in ['repo', 'catalog', 'repository']: + stack.set_visible_child(textview) + self._set_plugin_info_error(name) + return + if name in self._module_errors: stack.set_visible_child(textview) self._set_plugin_info_error(name) @@ -264,7 +295,10 @@ def _set_plugin_info_details(self, name): def _set_plugin_info_error(self, name): textview = self.gobjects['textview_plugin_info'] - exc, formatted_exc = self._module_errors[name] buf = textview.get_buffer() + if not name or name in ['repo', 'catalog', 'repository']: + buf.insert(buf.get_end_iter(), "{}\n\nPlease Select Plugin".format(name)) + return + exc, formatted_exc = self._module_errors[name] buf.insert(buf.get_end_iter(), "{0!r}\n\n".format(exc), -1) buf.insert(buf.get_end_iter(), ''.join(formatted_exc), -1) From 63369e070b26e59e9a552759e9f0da175f688e87 Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Fri, 6 Oct 2017 15:47:04 -0400 Subject: [PATCH 09/38] Added catalog manager --- data/client/king_phisher/client_config.json | 1 + .../king_phisher/king-phisher-client.ui | 2 +- king_phisher/catalog.py | 50 +++- king_phisher/client/plugins.py | 26 ++ king_phisher/client/windows/plugin_manager.py | 243 ++++++++++++++---- 5 files changed, 275 insertions(+), 47 deletions(-) diff --git a/data/client/king_phisher/client_config.json b/data/client/king_phisher/client_config.json index 92ee3434..51b23423 100644 --- a/data/client/king_phisher/client_config.json +++ b/data/client/king_phisher/client_config.json @@ -13,6 +13,7 @@ "filter.campaign.other_users": false, "plugins": {}, "plugins.enabled": [], + "plugins.installed": {}, "rpc.serializer": null, "server": "localhost:22", "server_remote_port": 80, diff --git a/data/client/king_phisher/king-phisher-client.ui b/data/client/king_phisher/king-phisher-client.ui index 9c49421e..caf60ef7 100644 --- a/data/client/king_phisher/king-phisher-client.ui +++ b/data/client/king_phisher/king-phisher-client.ui @@ -3002,7 +3002,7 @@ Spencer McIntyre - + True False Current Status diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index ad4ae109..be4511e6 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -117,7 +117,7 @@ class Repository(object): """ An object representing a single logical source of add on data. """ - __slots__ = ('__weakref__', '_req_sess', 'collections', 'description', 'security_keys', 'homepage', 'title', 'url_base') + __slots__ = ('__weakref__', 'id', '_req_sess', 'collections', 'description', 'security_keys', 'homepage', 'title', 'url_base') logger = logging.getLogger('KingPhisher.Catalog.Repository') def __init__(self, data, keys=None): """ @@ -133,6 +133,8 @@ def __init__(self, data, keys=None): self.description = data.get('description') self.homepage = data.get('homepage') """The URL of the homepage for this repository if it was specified.""" + self.id = data['id'] + """The unique id of this repository""" self.title = data['title'] """The title string of this repository.""" self.url_base = data['url-base'] @@ -297,7 +299,7 @@ class Catalog(object): data for the application. This information can then be loaded from an arbitrary source. """ - __slots__ = ('__weakref__', 'created', 'maintainers', 'repositories', 'security_keys') + __slots__ = ('__weakref__', 'id', 'created', 'maintainers', 'repositories', 'security_keys') logger = logging.getLogger('KingPhisher.Catalog') def __init__(self, data, keys=None): """ @@ -311,6 +313,8 @@ def __init__(self, data, keys=None): """The :py:class:`~king_phisher.security_keys.SecurityKeys` used for verifying remote data.""" self.created = dateutil.parser.parse(data['created']) """The timestamp of when the remote data was generated.""" + self.id = data['id'] + """The unique id of the catalog""" self.maintainers = tuple(maintainer['id'] for maintainer in data['maintainers']) """ A tuple containing the maintainers of the catalog and repositories. @@ -344,6 +348,48 @@ def from_url(cls, url, keys=None, encoding='utf-8'): keys.verify_dict(data, signature_encoding='base64') return cls(data, keys=keys) +class CatalogManager(object): + """ + Base manager for handling multiple Catalogs + """ + def __init__(self, url_catalog=None): + self.catalogs = {} + + if url_catalog: + self.add_catalog_url(url_catalog) + + def catalog_ids(self): + """ + The key names of the catalogs in the manager + :return: list + """ + return self.catalogs.keys() + + def get_repos(self, catalog_id): + """ + Returns a list of repositories from the requested catalog + :param str catalog_id: The name of the catalog in which to get names of repositories from + :return: list + """ + return [repository for repository in self.catalogs[catalog_id].repositories] + + def get_repo(self, catalog_id, repo_id): + """ + Returns the requested repository instance + :param str catalog_id: The name of the catalog the repo belongs to + :param str repo_id: The title of the repository requested. + :return: The repository instance + :rtype:py:class:Repository + """ + for repo in self.catalogs[catalog_id].repositories: + if repo.id != repo_id: + continue + return repo + + def add_catalog_url(self, url): + c = Catalog.from_url(url) + self.catalogs[c.id] = Catalog.from_url(url) + def sign_item_files(local_path, signing_key, signing_key_id, repo_path=None): """ This utility function is used to create a :py:class:`.CollectionItemFile` diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index b57bd1a2..620d6457 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -36,6 +36,7 @@ import weakref from king_phisher import plugins +from king_phisher import catalog from king_phisher.client import gui_utilities from king_phisher.client import mailer from king_phisher.client.widget import extras @@ -543,3 +544,28 @@ class ClientPluginManager(plugins.PluginManagerBase): _plugin_klass = ClientPlugin def __init__(self, path, application): super(ClientPluginManager, self).__init__(path, (application,)) + +class PluginCatalogManager(catalog.CatalogManager): + """ + Base manager for handling Catalogs + """ + def __init__(self, plugin_type, *args, **kwargs): + super(PluginCatalogManager, self).__init__(*args, **kwargs) + self.manager_type = 'plugins/' + plugin_type + + def get_collection(self, catalog_id, repo_id): + """ + Returns the manager type of the plugin collection of the requested catalog and repository. + + :param str catalog_id: The name of the catalog the repo belongs to + :param repo_id: The id of the repository requested. + :return: The the collection of manager type from the specified catalog and repository. + :rtype:py:class: + """ + if self.manager_type not in self.get_repo(catalog_id, repo_id).collections: + self.logger.warning('no plugins/client collection found in {}:{}'.format(catalog_id, repo_id)) + return + return self.get_repo(catalog_id, repo_id).collections[self.manager_type] + + def install_plugin(self, catalog_id, repo_id, plugin_id, install_path): + self.get_repo(catalog_id, repo_id).get_item_files(self.manager_type, plugin_id, install_path) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 988241ac..0dedb9d9 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -31,18 +31,22 @@ # import sys +import os import traceback from king_phisher import utilities from king_phisher.client import gui_utilities from king_phisher.client.widget import managers -from king_phisher import version +from king_phisher.client.plugins import PluginCatalogManager from gi.repository import Gdk from gi.repository import Gtk +from gi.repository import GLib __all__ = ('PluginManagerWindow',) +DEFAULT_PLUGIN_PATH = os.path.join(GLib.get_user_config_dir(), 'king-phisher', 'plugins') + class PluginManagerWindow(gui_utilities.GladeGObject): """ The window which allows the user to selectively enable and disable plugins @@ -65,7 +69,8 @@ class PluginManagerWindow(gui_utilities.GladeGObject): 'stack_plugin_info', 'treeview_plugins', 'textview_plugin_info', - 'viewport_plugin_info' + 'viewport_plugin_info', + 'statusbar' ) ) top_gobject = 'window' @@ -86,15 +91,25 @@ def __init__(self, *args, **kwargs): tvm.set_column_titles( ['Enabled', 'Installed', 'Type', 'Title', 'Compatible', 'Version'], column_offset=1, - renderers=[toggle_renderer_enable, toggle_renderer_install, Gtk.CellRendererText(), Gtk.CellRendererText(), Gtk.CellRendererText(), Gtk.CellRendererText()] + renderers=[ + toggle_renderer_enable, + toggle_renderer_install, + Gtk.CellRendererText(), + Gtk.CellRendererText(), + Gtk.CellRendererText(), + Gtk.CellRendererText() + ] ) tvm.column_views['Enabled'].set_cell_data_func(toggle_renderer_enable, self._toggle_cell_data_func) - #tvm.column_views['Installed'].set_cell_data_func(toggle_renderer_enable, self._toggle_cell_data_func) tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'visible', 7) - tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 7) - self._model = Gtk.TreeStore(str, bool, bool, str, str, str, str, bool) + tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 8) + self._model = Gtk.TreeStore(str, bool, bool, str, str, str, str, bool, bool) self._model.set_sort_column_id(2, Gtk.SortType.ASCENDING) treeview.set_model(self._model) + + #GLib.idle_add(self.gobjects['statusbar'].push, (0, 'Loading....')) + + self.catalog_plugins = PluginCatalogManager('client', 'https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json') self.load_plugins() self.popup_menu = tvm.get_popup_menu() @@ -122,9 +137,6 @@ def _toggle_cell_data_func(self, column, cell, model, tree_iter, _): else: cell.set_property('inconsistent', False) - def signal_popup_menu_activate_relaod_all(self, _): - self.load_plugins() - def load_plugins(self): """ Load the plugins which are available into the treeview to make them @@ -136,25 +148,59 @@ def load_plugins(self): self._module_errors = {} pm.load_all(on_error=self._on_plugin_load_error) #(parent (name, bool of check box, installed, type_name column, title column, compatible, version, bool if checkbox is visible)) - catalog = store.append(None, ('catalog', True, None, 'Catalog', 'Manually Installed', None, None, False)) - repo = store.append(catalog, ('repository', True, None, 'Repository', 'Local Paths', None, None, False)) for name, plugin in pm.loaded_plugins.items(): - store.append(repo, ( + if plugin.name in self.config['plugins.installed']: + continue + store.append(None, ( plugin.name, plugin.name in pm.enabled_plugins, True, 'Plugin', plugin.title, - 'Yes' if version.version >= plugin.req_min_version else 'No', + 'Yes' if plugin.is_compatible else 'No', plugin.version, - True + True, + False )) + for catalogs in self.catalog_plugins.catalog_ids(): + catalog = store.append(None, (catalogs, True, None, 'Catalog', catalogs, None, None, False, False)) + for repos in self.catalog_plugins.get_repos(catalogs): + repo = store.append(catalog, (repos.id, True, None, 'Repository', repos.title, None, None, False, False)) + plugin_collections = self.catalog_plugins.get_collection(catalogs, repos.id) + client_plugins = list(plugin_collections) + client_plugins.sort() + for plugins in client_plugins: + installed = False + enabled = False + if plugin_collections[plugins]['name'] in self.config['plugins.installed']: + if repos.id == self.config['plugins.installed'][plugin_collections[plugins]['name']][1]: + installed = True + enabled = True if plugin_collections[plugins]['name'] in self.config['plugins.enabled'] else False + store.append(repo, ( + plugin_collections[plugins]['name'], + enabled, + installed, + 'Plugin', + plugin_collections[plugins]['title'], + 'Unknown', + 'N/A', + True, + True + )) for name in self._module_errors.keys(): store.append(( name, False, "{0} (Load Failed)".format(name) )) + #GLib.idle_add(self.gobjects['statusbar'].pop, 0) + #GLib.idle_add(self.gobjects['statusbar'].push, (0, 'Finished loading')) + + def signal_popup_menu_activate_relaod_all(self, _): + self.load_plugins() + + def signal_treeview_row_activated(self, treeview, path, column): + self._set_plugin_info(self._model[path]) def signal_label_activate_link(self, _, uri): utilities.open_uri(uri) @@ -233,16 +279,22 @@ def signal_popup_menu_activate_reload(self, _): def signal_renderer_toggled_enable(self, _, path): pm = self.application.plugin_manager + if self._model[path][3] != 'Plugin': + return + plugin_model = self._model[path] name = self._model[path][0] # pylint: disable=unsubscriptable-object - if not name or name in ['repo', 'catalog', 'repository']: + if name not in pm.loaded_plugins: return + if name in self.config['plugins.installed']: + installed_plugin_info = self.config['plugins.installed'][name] + model_repo, model_cat = self._get_plugin_model_parents(self._model[path]) + if model_repo[0] != installed_plugin_info[1] or model_cat[0] != installed_plugin_info[0]: + return if name in self._module_errors: gui_utilities.show_dialog_error('Can Not Enable Plugin', self.window, 'Can not enable a plugin which failed to load.') return if self._model[path][1]: # pylint: disable=unsubscriptable-object - pm.disable(name) - self._model[path][1] = False # pylint: disable=unsubscriptable-object - self.config['plugins.enabled'].remove(name) + self._disable_plugin(plugin_model) else: if not pm.loaded_plugins[name].is_compatible: gui_utilities.show_dialog_error('Incompatible Plugin', self.window, 'This plugin is not compatible.') @@ -253,52 +305,155 @@ def signal_renderer_toggled_enable(self, _, path): self.config['plugins.enabled'].append(name) def signal_renderer_toggled_install(self, _, path): - pass + plugin_model = self._model[path] + repo_model, catalog_model = self._get_plugin_model_parents(plugin_model) + plugin_collection = self.catalog_plugins.get_collection(catalog_model[0], repo_model[0]) + if plugin_model[2]: + if plugin_model[1]: + response = gui_utilities.show_dialog_yes_no( + 'Plugin is enabled', + self.window, + "This will disable the plugin, do you wish to continue?" + ) + if not response: + return + self._disable_plugin(plugin_model) + self.application.plugin_manager.unload(plugin_model[0]) + self._uninstall_plugin(plugin_collection, plugin_model) + self.logger.info("uninstalled plugin {}".format(plugin_model[0])) + else: + if plugin_model[0] in self.config['plugins.installed']: + installed_plugin_info = self.config['plugins.installed'][plugin_model[0]] + if installed_plugin_info != [catalog_model[0], repo_model[0]]: + response = gui_utilities.show_dialog_yes_no( + 'Plugin installed from another source', + self.window, + "A plugin with this name is already installed from Catalog: {} Repo: {}\nDo you want to replace it with this one?".format(installed_plugin_info[0], installed_plugin_info[1]) + ) + if not response: + return + if not self._remove_matching_plugin(plugin_model[0], installed_plugin_info): + self.logger.warning('failed to uninstall plugin {}'.format(plugin_model[0])) + return + self.catalog_plugins.install_plugin(catalog_model[0], repo_model[0], plugin_model[0], DEFAULT_PLUGIN_PATH) + self.config['plugins.installed'][plugin_model[0]] = [catalog_model[0], repo_model[0]] + plugin_model[2] = True + self.logger.info("installed plugin {} from catalog {}, repository {}".format(plugin_model[0], catalog_model[0], repo_model[0])) + self.application.plugin_manager.load_all(on_error=self._on_plugin_load_error) - def signal_treeview_row_activated(self, treeview, path, column): - name = self._model[path][0] # pylint: disable=unsubscriptable-object - self._set_plugin_info(name) + def _disable_plugin(self, plugin_model): + self.application.plugin_manager.disable(plugin_model[0]) + self.config['plugins.enabled'].remove(plugin_model[0]) + plugin_model[1] = False - def _set_plugin_info(self, name): + def _remove_matching_plugin(self, plugin_name, installed_plugin_info): + for catalog_model in self._model: + if catalog_model[0] != installed_plugin_info[0]: + continue + for repo_model in catalog_model.iterchildren(): + if repo_model[0] != installed_plugin_info[1]: + continue + for plugin_model in repo_model.iterchildren(): + if plugin_model[0] != plugin_name: + continue + if plugin_model[1]: + self._disable_plugin(plugin_model) + self._uninstall_plugin(self.catalog_plugins.get_collection(installed_plugin_info[0], installed_plugin_info[1]), plugin_model) + return True + + def _get_plugin_model_parents(self, plugin_model): + return plugin_model.parent, plugin_model.parent.parent + + def _uninstall_plugin(self, plugin_collection, plugin_model): + for files in plugin_collection[plugin_model[0]]['files']: + file_name = files[0] + if os.path.isfile(os.path.join(DEFAULT_PLUGIN_PATH, file_name)): + os.remove(os.path.join(DEFAULT_PLUGIN_PATH, file_name)) + del self.config['plugins.installed'][plugin_model[0]] + plugin_model[2] = False + + def _set_plugin_info(self, model_instance): stack = self.gobjects['stack_plugin_info'] textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() buf.delete(buf.get_start_iter(), buf.get_end_iter()) - if not name or name in ['repo', 'catalog', 'repository']: + name = model_instance[0] + if model_instance[3] != 'Plugin': stack.set_visible_child(textview) - self._set_plugin_info_error(name) + self._set_non_plugin_info(model_instance) return - if name in self._module_errors: stack.set_visible_child(textview) self._set_plugin_info_error(name) else: stack.set_visible_child(self.gobjects['grid_plugin_info']) - self._set_plugin_info_details(name) + self._set_plugin_info_details(model_instance) + + def _set_non_plugin_info(self, model_instance): + textview = self.gobjects['textview_plugin_info'] + buf = textview.get_buffer() + text = '' + if model_instance[3] == 'Catalog': + instance_information = self.catalog_plugins.catalogs[model_instance[0]] + else: + instance_information = self.catalog_plugins.get_repo(model_instance.parent[0], model_instance[0]) + + if 'title' in dir(instance_information): + text += "Repository: {}\n".format(instance_information.title if instance_information.title else instance_information.id) + else: + text += "Catalog: {}\n".format(instance_information.id) - def _set_plugin_info_details(self, name): + if 'maintainers' in dir(instance_information): + if instance_information.maintainers: + text += 'maintainer: ' + '\nmaintainer: '.join(instance_information.maintainers) + '\n' + + if 'description' in dir(instance_information): + if instance_information.description: + text += instance_information.description + '\n' + + buf.insert(buf.get_end_iter(), "{0}\n".format(text), -1) + + def _set_plugin_info_details(self, plugin_model): + name = plugin_model[0] pm = self.application.plugin_manager self._last_plugin_selected = name - klass = pm.loaded_plugins[name] - self.gobjects['label_plugin_info_title'].set_text(klass.title) - self.gobjects['label_plugin_info_compatible'].set_text('Yes' if klass.is_compatible else 'No') - self.gobjects['label_plugin_info_version'].set_text(klass.version) - self.gobjects['label_plugin_info_authors'].set_text('\n'.join(klass.authors)) - label_homepage = self.gobjects['label_plugin_info_homepage'] - if klass.homepage is None: - label_homepage.set_property('visible', False) + if name in pm.loaded_plugins: + klass = pm.loaded_plugins[name] + self.gobjects['label_plugin_info_title'].set_text(klass.title) + self.gobjects['label_plugin_info_compatible'].set_text('Yes' if klass.is_compatible else 'No') + self.gobjects['label_plugin_info_version'].set_text(klass.version) + self.gobjects['label_plugin_info_authors'].set_text('\n'.join(klass.authors)) + label_homepage = self.gobjects['label_plugin_info_homepage'] + if klass.homepage is None: + label_homepage.set_property('visible', False) + else: + label_homepage.set_markup("Homepage".format(klass.homepage)) + label_homepage.set_property('tooltip-text', klass.homepage) + label_homepage.set_property('visible', True) + self.gobjects['label_plugin_info_description'].set_text(klass.description) else: - label_homepage.set_markup("Homepage".format(klass.homepage)) - label_homepage.set_property('tooltip-text', klass.homepage) - label_homepage.set_property('visible', True) - self.gobjects['label_plugin_info_description'].set_text(klass.description) + repo_model, catalog_model = self._get_plugin_model_parents(plugin_model) + for repo in self.catalog_plugins.get_repos(catalog_model[0]): + if repo.id != repo_model[0]: + continue + plugin = repo.collections['plugins/client'][plugin_model[0]] + self.gobjects['label_plugin_info_title'].set_text(plugin['title']) + self.gobjects['label_plugin_info_compatible'].set_text('Fix ME Please') #fix me + self.gobjects['label_plugin_info_version'].set_text(plugin['version']) + self.gobjects['label_plugin_info_authors'].set_text('\n'.join(plugin['authors'])) + label_homepage = self.gobjects['label_plugin_info_homepage'] + if plugin['homepage'] is None: + label_homepage.set_property('visible', False) + else: + label_homepage.set_markup("Homepage".format(plugin['homepage'])) + label_homepage.set_property('tooltip-text', plugin['homepage']) + label_homepage.set_property('visible', True) + self.gobjects['label_plugin_info_description'].set_text(plugin['description']) - def _set_plugin_info_error(self, name): + def _set_plugin_info_error(self, model_instance): + name = model_instance[0] textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() - if not name or name in ['repo', 'catalog', 'repository']: - buf.insert(buf.get_end_iter(), "{}\n\nPlease Select Plugin".format(name)) - return exc, formatted_exc = self._module_errors[name] buf.insert(buf.get_end_iter(), "{0!r}\n\n".format(exc), -1) buf.insert(buf.get_end_iter(), ''.join(formatted_exc), -1) From e874d06a8671ea5dd3bfcac01ff17111d7f67a40 Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Wed, 11 Oct 2017 15:08:18 -0400 Subject: [PATCH 10/38] Catalog and cache manager --- .../king_phisher/king-phisher-client.ui | 1 + king_phisher/catalog.py | 12 +- king_phisher/client/plugins.py | 192 +++++++++++++++++- king_phisher/client/windows/plugin_manager.py | 110 ++++++---- king_phisher/plugins.py | 2 +- 5 files changed, 273 insertions(+), 44 deletions(-) diff --git a/data/client/king_phisher/king-phisher-client.ui b/data/client/king_phisher/king-phisher-client.ui index caf60ef7..f802077c 100644 --- a/data/client/king_phisher/king-phisher-client.ui +++ b/data/client/king_phisher/king-phisher-client.ui @@ -2726,6 +2726,7 @@ Spencer McIntyre Plugin Manager center-on-parent True + True diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index be4511e6..120cd10b 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -361,7 +361,8 @@ def __init__(self, url_catalog=None): def catalog_ids(self): """ The key names of the catalogs in the manager - :return: list + :return: the catalogs ids in CatalogManager + :rtype: list """ return self.catalogs.keys() @@ -377,7 +378,7 @@ def get_repo(self, catalog_id, repo_id): """ Returns the requested repository instance :param str catalog_id: The name of the catalog the repo belongs to - :param str repo_id: The title of the repository requested. + :param str repo_id: The id of the repository requested. :return: The repository instance :rtype:py:class:Repository """ @@ -387,8 +388,11 @@ def get_repo(self, catalog_id, repo_id): return repo def add_catalog_url(self, url): - c = Catalog.from_url(url) - self.catalogs[c.id] = Catalog.from_url(url) + try: + c = Catalog.from_url(url) + self.catalogs[c.id] = c + except Exception as error: + logging.warning("failed to load catalog from url {} due to {}".format(url, error)) def sign_item_files(local_path, signing_key, signing_key_id, repo_path=None): """ diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 620d6457..21c40821 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -29,20 +29,28 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # - +import distutils.version import collections +import json import os import tempfile import weakref +import pip from king_phisher import plugins from king_phisher import catalog +from king_phisher import version from king_phisher.client import gui_utilities from king_phisher.client import mailer from king_phisher.client.widget import extras from gi.repository import Gtk import jinja2.exceptions +from gi.repository import GLib + +DEFAULT_CONFIG_PATH = os.path.join(GLib.get_user_config_dir(), 'king-phisher') + +StrictVersion = distutils.version.StrictVersion def _split_menu_path(menu_path): menu_path = [path_item.strip() for path_item in menu_path.split('>')] @@ -545,11 +553,137 @@ class ClientPluginManager(plugins.PluginManagerBase): def __init__(self, path, application): super(ClientPluginManager, self).__init__(path, (application,)) +class RepoCache(object): + """ + RepoCache is used to hold basic information on repos that is to be cached, or pulled from cache. + """ + __slots__ = ('_id', 'title', 'collections_types', 'url') + + def __init__(self, _id, title, collections_types, url): + self._id = _id + self.title = title + self.collections_types = collections_types + self.url = url + + @property + def id(self): + return self._id + + @property + def collections(self): + return self.collections_types + + def to_dict(self): + repo_cache_values = { + 'id': self._id, + 'title': self.title, + 'collections': self.collections_types, + 'url': self.url + } + return repo_cache_values + +class CatalogCache(object): + """ + CatalogCache is used to hold basic information on catalogs that is to be cached or pulled from cache. + """ + __slots__ = ('_id', 'repos') + + def __init__(self, _id, repos): + self._id = _id + self.repos = {} + for repo in repos: + self.repos[repo['id']] = RepoCache( + repo['id'], + repo['title'], + repo['collections'], + repo['url'] + ) + + def __getitem__(self, key): + return self.repos[key] + + def __iter__(self): + for repo in self.repos: + yield self.repos[repo] + + @property + def id(self): + return self._id + + def to_dict(self): + catalog_cache_dict = { + 'id': self._id, + 'repos': [self.repos[repo].to_dict() for repo in self.repos] + } + return catalog_cache_dict + +class CatalogCacheManager(collections.MutableMapping): + """ + Manager to handle cache information for catalogs + """ + def __init__(self, cache_file): + self._data = {} + self._cache_dict = {} + self._cache_cat = {} + self._cache_file = cache_file + + if os.path.isfile(cache_file): + with open(cache_file) as file_h: + try: + self._cache_dict = json.load(file_h) + except ValueError: + self._cache_dict = {} + + if not self._cache_dict or 'catalogs' not in self._cache_dict: + self._cache_dict['catalogs'] = {} + else: + cache_cat = self._cache_dict['catalogs'] + for catalog_ in cache_cat: + self[catalog_] = CatalogCache( + cache_cat[catalog_]['id'], + cache_cat[catalog_]['repos'] + ) + + def __setitem__(self, key, value): + self._data[key] = value + + def __getitem__(self, key): + return self._data[key] + + def __delitem__(self, key): + del self._data[key] + + def __len__(self): + return len(self._data) + + def __iter__(self): + for key in self._data.keys(): + yield key + + def add_catalog_cache(self, cat_id, repos): + self[cat_id] = CatalogCache( + cat_id, + repos + ) + + def to_dict(self): + cache = {} + for key in self: + cache[key] = self._data[key].to_dict() + return cache + + def save(self): + self._cache_dict['catalogs'] = self.to_dict() + with open(self._cache_file, 'w+') as file_h: + json.dump(self._cache_dict, file_h, sort_keys=True, indent=4) + class PluginCatalogManager(catalog.CatalogManager): """ Base manager for handling Catalogs """ + def __init__(self, plugin_type, *args, **kwargs): + self._catalog_cache = CatalogCacheManager(os.path.join(DEFAULT_CONFIG_PATH, 'cache.json')) super(PluginCatalogManager, self).__init__(*args, **kwargs) self.manager_type = 'plugins/' + plugin_type @@ -563,9 +697,63 @@ def get_collection(self, catalog_id, repo_id): :rtype:py:class: """ if self.manager_type not in self.get_repo(catalog_id, repo_id).collections: - self.logger.warning('no plugins/client collection found in {}:{}'.format(catalog_id, repo_id)) return return self.get_repo(catalog_id, repo_id).collections[self.manager_type] def install_plugin(self, catalog_id, repo_id, plugin_id, install_path): self.get_repo(catalog_id, repo_id).get_item_files(self.manager_type, plugin_id, install_path) + + def save_catalog_cache(self): + for catalog_ in self.catalogs: + if catalog_ not in self._catalog_cache: + self._catalog_cache.add_catalog_cache( + self.catalogs[catalog_].id, + self.get_repos_to_cache(catalog_), + ) + self._catalog_cache.save() + + def add_catalog_url(self, url): + super().add_catalog_url(url) + if self.catalogs: + self.save_catalog_cache() + + def is_compatible(self, catalog_id, repo_id, plugin_name): + plugin = self.get_collection(catalog_id, repo_id)[plugin_name] + requirements = plugin['requirements'] + if requirements['minimum-version'] is not None: + if StrictVersion(requirements['minimum-version']) > StrictVersion(version.distutils_version): + return False + if requirements['packages']: + if not all(self._package_check(requirements['packages'])): + return False + return True + + def _package_check(self, packages): + installed_packages = sorted(i.key for i in pip.get_installed_distributions()) + for package in packages: + if package not in installed_packages: + yield False + else: + yield True + + def get_repos_to_cache(self, catalog_): + repo_cache_info = [] + for repo in self.get_repos(catalog_): + repo_cache_info.append({ + 'id': repo.id, + 'title': repo.title, + 'collections': [collection_ for collection_ in repo.collections], + 'url': repo.url_base + }) + return repo_cache_info + + def get_cache(self): + return self._catalog_cache + + def get_cache_catalog_ids(self): + for item in self._catalog_cache: + yield item + + def get_cache_repos(self, catalog_id): + for repos in self._catalog_cache[catalog_id]: + yield repos diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 0dedb9d9..3ff18a77 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -38,7 +38,7 @@ from king_phisher.client import gui_utilities from king_phisher.client.widget import managers from king_phisher.client.plugins import PluginCatalogManager - +from requests import exceptions as rexceptions from gi.repository import Gdk from gi.repository import Gtk from gi.repository import GLib @@ -107,9 +107,9 @@ def __init__(self, *args, **kwargs): self._model.set_sort_column_id(2, Gtk.SortType.ASCENDING) treeview.set_model(self._model) - #GLib.idle_add(self.gobjects['statusbar'].push, (0, 'Loading....')) - self.catalog_plugins = PluginCatalogManager('client', 'https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json') + self.logger.warning("failed to connect to catalog server") + self.load_plugins() self.popup_menu = tvm.get_popup_menu() @@ -147,9 +147,8 @@ def load_plugins(self): pm = self.application.plugin_manager self._module_errors = {} pm.load_all(on_error=self._on_plugin_load_error) - #(parent (name, bool of check box, installed, type_name column, title column, compatible, version, bool if checkbox is visible)) for name, plugin in pm.loaded_plugins.items(): - if plugin.name in self.config['plugins.installed']: + if name in self.config['plugins.installed']: continue store.append(None, ( plugin.name, @@ -162,43 +161,80 @@ def load_plugins(self): True, False )) - for catalogs in self.catalog_plugins.catalog_ids(): - catalog = store.append(None, (catalogs, True, None, 'Catalog', catalogs, None, None, False, False)) - for repos in self.catalog_plugins.get_repos(catalogs): - repo = store.append(catalog, (repos.id, True, None, 'Repository', repos.title, None, None, False, False)) - plugin_collections = self.catalog_plugins.get_collection(catalogs, repos.id) - client_plugins = list(plugin_collections) - client_plugins.sort() - for plugins in client_plugins: - installed = False - enabled = False - if plugin_collections[plugins]['name'] in self.config['plugins.installed']: - if repos.id == self.config['plugins.installed'][plugin_collections[plugins]['name']][1]: - installed = True - enabled = True if plugin_collections[plugins]['name'] in self.config['plugins.enabled'] else False - store.append(repo, ( - plugin_collections[plugins]['name'], - enabled, - installed, - 'Plugin', - plugin_collections[plugins]['title'], - 'Unknown', - 'N/A', - True, - True - )) for name in self._module_errors.keys(): store.append(( name, False, "{0} (Load Failed)".format(name) )) - #GLib.idle_add(self.gobjects['statusbar'].pop, 0) - #GLib.idle_add(self.gobjects['statusbar'].push, (0, 'Finished loading')) + + if not self.catalog_plugins.catalog_ids(): + return + if not self.catalog_plugins.get_cache(): + return + + if self.catalog_plugins.catalog_ids(): + for catalogs in self.catalog_plugins.catalog_ids(): + catalog = store.append(None, (catalogs, True, None, 'Catalog', catalogs, None, None, False, False)) + for repos in self.catalog_plugins.get_repos(catalogs): + repo = store.append(catalog, (repos.id, True, None, 'Repository', repos.title, None, None, False, False)) + plugin_collections = self.catalog_plugins.get_collection(catalogs, repos.id) + client_plugins = list(plugin_collections) + client_plugins.sort() + for plugins in client_plugins: + installed = False + enabled = False + if plugin_collections[plugins]['name'] in self.config['plugins.installed']: + if repos.id == self.config['plugins.installed'][plugin_collections[plugins]['name']][1]: + installed = True + enabled = True if plugin_collections[plugins]['name'] in self.config['plugins.enabled'] else False + store.append(repo, ( + plugins, + enabled, + installed, + 'Plugin', + plugin_collections[plugins]['title'], + 'Yes' if self.catalog_plugins.is_compatible(catalogs, repos.id, plugins) else 'No', + self.compare_plugin_versions(plugin_collections[plugins]['name'], plugin_collections[plugins]['version']), + True, + True + )) + else: + for catalog_id in self.catalog_plugins.get_cache_catalog_ids(): + catalog_line = store.append(None, (catalog_id, True, None, 'Catalog (offline)', catalog_id, None, None, False, False)) + for repo in self.catalog_plugins.get_cache_repos(catalog_id): + repo_line = store.append(catalog_line, (repo.id, True, None, 'Repository (offline)', repo.title, None, None, False, False)) + for plugin in self.config['plugins.installed']: + if self.config['plugins.installed'][plugin][0] != catalog_id: + continue + if self.config['plugins.installed'][plugin][1] != repo.id: + continue + store.append(repo_line, ( + plugin, + True if plugin in self.config['plugins.enabled'] else False, + True, + 'Plugin', + pm[plugin].title, + 'Yes' if pm[plugin].is_compatible else 'No', + pm[plugin].version, + True, + False + )) + + def compare_plugin_versions(self, plugin_name, plugin_version): + if plugin_name not in self.application.plugin_manager: + return plugin_version + if self.application.plugin_manager[plugin_name].version < plugin_version: + return "Upgrade available" + return self.application.plugin_manager[plugin_name].version def signal_popup_menu_activate_relaod_all(self, _): self.load_plugins() + def signal_destory(self, _): + if self.catalog_plugins: + self.catalog_plugins.save_catalog_cache() + def signal_treeview_row_activated(self, treeview, path, column): self._set_plugin_info(self._model[path]) @@ -285,7 +321,7 @@ def signal_renderer_toggled_enable(self, _, path): name = self._model[path][0] # pylint: disable=unsubscriptable-object if name not in pm.loaded_plugins: return - if name in self.config['plugins.installed']: + if self._model[path].parent: installed_plugin_info = self.config['plugins.installed'][name] model_repo, model_cat = self._get_plugin_model_parents(self._model[path]) if model_repo[0] != installed_plugin_info[1] or model_cat[0] != installed_plugin_info[0]: @@ -318,7 +354,6 @@ def signal_renderer_toggled_install(self, _, path): if not response: return self._disable_plugin(plugin_model) - self.application.plugin_manager.unload(plugin_model[0]) self._uninstall_plugin(plugin_collection, plugin_model) self.logger.info("uninstalled plugin {}".format(plugin_model[0])) else: @@ -338,7 +373,8 @@ def signal_renderer_toggled_install(self, _, path): self.catalog_plugins.install_plugin(catalog_model[0], repo_model[0], plugin_model[0], DEFAULT_PLUGIN_PATH) self.config['plugins.installed'][plugin_model[0]] = [catalog_model[0], repo_model[0]] plugin_model[2] = True - self.logger.info("installed plugin {} from catalog {}, repository {}".format(plugin_model[0], catalog_model[0], repo_model[0])) + plugin_model[6] = self.catalog_plugins.get_collection(catalog_model[0], repo_model[0])[plugin_model[0]]['version'] + self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(plugin_model[0], catalog_model[0], repo_model[0])) self.application.plugin_manager.load_all(on_error=self._on_plugin_load_error) def _disable_plugin(self, plugin_model): @@ -405,7 +441,7 @@ def _set_non_plugin_info(self, model_instance): if 'maintainers' in dir(instance_information): if instance_information.maintainers: - text += 'maintainer: ' + '\nmaintainer: '.join(instance_information.maintainers) + '\n' + text += 'Maintainer: ' + '\nMaintainer: '.join(instance_information.maintainers) + '\n' if 'description' in dir(instance_information): if instance_information.description: @@ -438,7 +474,7 @@ def _set_plugin_info_details(self, plugin_model): continue plugin = repo.collections['plugins/client'][plugin_model[0]] self.gobjects['label_plugin_info_title'].set_text(plugin['title']) - self.gobjects['label_plugin_info_compatible'].set_text('Fix ME Please') #fix me + self.gobjects['label_plugin_info_compatible'].set_text('Yes' if self.catalog_plugins.is_compatible(catalog_model[0], repo_model[0], plugin_model[0]) else 'No') self.gobjects['label_plugin_info_version'].set_text(plugin['version']) self.gobjects['label_plugin_info_authors'].set_text('\n'.join(plugin['authors'])) label_homepage = self.gobjects['label_plugin_info_homepage'] diff --git a/king_phisher/plugins.py b/king_phisher/plugins.py index 688d8fd2..c5148e45 100644 --- a/king_phisher/plugins.py +++ b/king_phisher/plugins.py @@ -422,7 +422,7 @@ def load_module(self, name, reload_module=False): recursive_reload(module) return module - def unload(self, name): + def PluginBaseMeta(self, name): """ Unload a plugin from memory. If the specified plugin is currently enabled, it will first be disabled before being unloaded. If the plugin From 159bfbfb30239ef6c4e949853b953c6cae80f0d2 Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Tue, 24 Oct 2017 15:54:32 -0400 Subject: [PATCH 11/38] Refactor to namedtuples and fixed based comments --- king_phisher/catalog.py | 4 +- king_phisher/client/plugins.py | 165 +++------ king_phisher/client/windows/plugin_manager.py | 332 +++++++++++------- king_phisher/plugins.py | 66 +++- 4 files changed, 323 insertions(+), 244 deletions(-) diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index 120cd10b..28cb4ebc 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -366,7 +366,7 @@ def catalog_ids(self): """ return self.catalogs.keys() - def get_repos(self, catalog_id): + def get_repositories(self, catalog_id): """ Returns a list of repositories from the requested catalog :param str catalog_id: The name of the catalog in which to get names of repositories from @@ -374,7 +374,7 @@ def get_repos(self, catalog_id): """ return [repository for repository in self.catalogs[catalog_id].repositories] - def get_repo(self, catalog_id, repo_id): + def get_repository(self, catalog_id, repo_id): """ Returns the requested repository instance :param str catalog_id: The name of the catalog the repo belongs to diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 21c40821..25eabf4f 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -29,28 +29,31 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -import distutils.version + import collections import json import os +import sys import tempfile import weakref -import pip from king_phisher import plugins from king_phisher import catalog -from king_phisher import version from king_phisher.client import gui_utilities from king_phisher.client import mailer from king_phisher.client.widget import extras +from king_phisher.plugins import Requirements from gi.repository import Gtk import jinja2.exceptions from gi.repository import GLib -DEFAULT_CONFIG_PATH = os.path.join(GLib.get_user_config_dir(), 'king-phisher') +if sys.version_info[:3] >= (3, 3, 0): + _MutableMapping = collections.abc.MutableMapping +else: + _MutableMapping = collections.MutableMapping -StrictVersion = distutils.version.StrictVersion +DEFAULT_CONFIG_PATH = os.path.join(GLib.get_user_config_dir(), 'king-phisher') def _split_menu_path(menu_path): menu_path = [path_item.strip() for path_item in menu_path.split('>')] @@ -553,79 +556,15 @@ class ClientPluginManager(plugins.PluginManagerBase): def __init__(self, path, application): super(ClientPluginManager, self).__init__(path, (application,)) -class RepoCache(object): - """ - RepoCache is used to hold basic information on repos that is to be cached, or pulled from cache. - """ - __slots__ = ('_id', 'title', 'collections_types', 'url') - - def __init__(self, _id, title, collections_types, url): - self._id = _id - self.title = title - self.collections_types = collections_types - self.url = url - - @property - def id(self): - return self._id - - @property - def collections(self): - return self.collections_types - - def to_dict(self): - repo_cache_values = { - 'id': self._id, - 'title': self.title, - 'collections': self.collections_types, - 'url': self.url - } - return repo_cache_values - -class CatalogCache(object): - """ - CatalogCache is used to hold basic information on catalogs that is to be cached or pulled from cache. - """ - __slots__ = ('_id', 'repos') - - def __init__(self, _id, repos): - self._id = _id - self.repos = {} - for repo in repos: - self.repos[repo['id']] = RepoCache( - repo['id'], - repo['title'], - repo['collections'], - repo['url'] - ) - - def __getitem__(self, key): - return self.repos[key] - - def __iter__(self): - for repo in self.repos: - yield self.repos[repo] - - @property - def id(self): - return self._id - - def to_dict(self): - catalog_cache_dict = { - 'id': self._id, - 'repos': [self.repos[repo].to_dict() for repo in self.repos] - } - return catalog_cache_dict - -class CatalogCacheManager(collections.MutableMapping): +class CatalogCacheManager(object): """ Manager to handle cache information for catalogs """ def __init__(self, cache_file): - self._data = {} self._cache_dict = {} - self._cache_cat = {} - self._cache_file = cache_file + self._data = {} + self._CatalogCacheEntry = collections.namedtuple('catalog', ['id', 'collections']) + self._CollectionCacheEntry = collections.namedtuple('collection', ['id', 'title', 'url', 'types']) if os.path.isfile(cache_file): with open(cache_file) as file_h: @@ -633,17 +572,29 @@ def __init__(self, cache_file): self._cache_dict = json.load(file_h) except ValueError: self._cache_dict = {} + self._cache_file = cache_file if not self._cache_dict or 'catalogs' not in self._cache_dict: self._cache_dict['catalogs'] = {} else: cache_cat = self._cache_dict['catalogs'] for catalog_ in cache_cat: - self[catalog_] = CatalogCache( + self[catalog_] = self._CatalogCacheEntry( cache_cat[catalog_]['id'], - cache_cat[catalog_]['repos'] + self._tuple_collections(cache_cat[catalog_]['collections']) ) + def _tuple_collections(self, list_collections): + collections_ = [] + for collection_ in list_collections: + collections_.append(self._CollectionCacheEntry( + collection_['id'], + collection_['title'], + collection_['url'], + collection_['types'] + )) + return collections_ + def __setitem__(self, key, value): self._data[key] = value @@ -660,16 +611,21 @@ def __iter__(self): for key in self._data.keys(): yield key - def add_catalog_cache(self, cat_id, repos): - self[cat_id] = CatalogCache( + def add_catalog_cache(self, cat_id, collections_): + self[cat_id] = self._CatalogCacheEntry( cat_id, - repos + self._tuple_collections(collections_) ) def to_dict(self): cache = {} for key in self: - cache[key] = self._data[key].to_dict() + cache[key] = {} + cache[key]['id'] = self[key].id + collections_ = [] + for collection_ in self[key].collections: + collections_.append(dict(collection_._asdict())) + cache[key]['collections'] = collections_ return cache def save(self): @@ -677,15 +633,17 @@ def save(self): with open(self._cache_file, 'w+') as file_h: json.dump(self._cache_dict, file_h, sort_keys=True, indent=4) -class PluginCatalogManager(catalog.CatalogManager): +class ClientCatalogManager(catalog.CatalogManager): """ Base manager for handling Catalogs """ - def __init__(self, plugin_type, *args, **kwargs): + def __init__(self, manager_type=None, *args, **kwargs): self._catalog_cache = CatalogCacheManager(os.path.join(DEFAULT_CONFIG_PATH, 'cache.json')) - super(PluginCatalogManager, self).__init__(*args, **kwargs) - self.manager_type = 'plugins/' + plugin_type + super(ClientCatalogManager, self).__init__(*args, **kwargs) + self.manager_type = 'plugins/client' + if manager_type: + self.manager_type = manager_type def get_collection(self, catalog_id, repo_id): """ @@ -696,19 +654,19 @@ def get_collection(self, catalog_id, repo_id): :return: The the collection of manager type from the specified catalog and repository. :rtype:py:class: """ - if self.manager_type not in self.get_repo(catalog_id, repo_id).collections: + if self.manager_type not in self.get_repository(catalog_id, repo_id).collections: return - return self.get_repo(catalog_id, repo_id).collections[self.manager_type] + return self.get_repository(catalog_id, repo_id).collections[self.manager_type] def install_plugin(self, catalog_id, repo_id, plugin_id, install_path): - self.get_repo(catalog_id, repo_id).get_item_files(self.manager_type, plugin_id, install_path) + self.get_repository(catalog_id, repo_id).get_item_files(self.manager_type, plugin_id, install_path) def save_catalog_cache(self): for catalog_ in self.catalogs: if catalog_ not in self._catalog_cache: self._catalog_cache.add_catalog_cache( self.catalogs[catalog_].id, - self.get_repos_to_cache(catalog_), + self.get_collections_to_cache(catalog_), ) self._catalog_cache.save() @@ -719,26 +677,15 @@ def add_catalog_url(self, url): def is_compatible(self, catalog_id, repo_id, plugin_name): plugin = self.get_collection(catalog_id, repo_id)[plugin_name] - requirements = plugin['requirements'] - if requirements['minimum-version'] is not None: - if StrictVersion(requirements['minimum-version']) > StrictVersion(version.distutils_version): - return False - if requirements['packages']: - if not all(self._package_check(requirements['packages'])): - return False - return True - - def _package_check(self, packages): - installed_packages = sorted(i.key for i in pip.get_installed_distributions()) - for package in packages: - if package not in installed_packages: - yield False - else: - yield True - - def get_repos_to_cache(self, catalog_): + return Requirements(plugin['requirements']).is_compatible + + def compatibility(self, catalog_id, repo_id, plugin_name): + plugin = self.get_collection(catalog_id, repo_id)[plugin_name] + return Requirements(plugin['requirements']).compatibility + + def get_collections_to_cache(self, catalog_): repo_cache_info = [] - for repo in self.get_repos(catalog_): + for repo in self.get_repositories(catalog_): repo_cache_info.append({ 'id': repo.id, 'title': repo.title, @@ -754,6 +701,6 @@ def get_cache_catalog_ids(self): for item in self._catalog_cache: yield item - def get_cache_repos(self, catalog_id): - for repos in self._catalog_cache[catalog_id]: - yield repos + def get_cache_collections(self, catalog_id): + for collections_ in self._catalog_cache[catalog_id].collections: + yield collections_ diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 3ff18a77..c522e0d3 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -34,11 +34,12 @@ import os import traceback +from collections import namedtuple + from king_phisher import utilities from king_phisher.client import gui_utilities from king_phisher.client.widget import managers -from king_phisher.client.plugins import PluginCatalogManager -from requests import exceptions as rexceptions +from king_phisher.client.plugins import ClientCatalogManager from gi.repository import Gdk from gi.repository import Gtk from gi.repository import GLib @@ -103,11 +104,25 @@ def __init__(self, *args, **kwargs): tvm.column_views['Enabled'].set_cell_data_func(toggle_renderer_enable, self._toggle_cell_data_func) tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'visible', 7) tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 8) + self._named_model = namedtuple( + 'model_row', + [ + 'id', + 'enabled', + 'installed', + 'type', + 'title', + 'compatibility', + 'version', + 'visible_enabled', + 'visible_installed', + ] + ) self._model = Gtk.TreeStore(str, bool, bool, str, str, str, str, bool, bool) self._model.set_sort_column_id(2, Gtk.SortType.ASCENDING) treeview.set_model(self._model) - self.catalog_plugins = PluginCatalogManager('client', 'https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json') + self.catalog_plugins = ClientCatalogManager(url_catalog='https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json') self.logger.warning("failed to connect to catalog server") self.load_plugins() @@ -128,6 +143,13 @@ def __init__(self, *args, **kwargs): paned = self.gobjects['paned_plugins'] self._paned_offset = paned.get_allocation().height - paned.get_position() + def _model_item(self, model_path, item): + named_row = self._named_model(*self._model[model_path]) + return getattr(named_row, item) + + def _set_model_item(self, model_path, item, item_value): + self._model[model_path][self._named_model._fields.index(item)] = item_value + def _on_plugin_load_error(self, name, error): self._module_errors[name] = (error, traceback.format_exception(*sys.exc_info(), limit=5)) @@ -168,58 +190,60 @@ def load_plugins(self): "{0} (Load Failed)".format(name) )) - if not self.catalog_plugins.catalog_ids(): - return - if not self.catalog_plugins.get_cache(): - return - if self.catalog_plugins.catalog_ids(): for catalogs in self.catalog_plugins.catalog_ids(): catalog = store.append(None, (catalogs, True, None, 'Catalog', catalogs, None, None, False, False)) - for repos in self.catalog_plugins.get_repos(catalogs): - repo = store.append(catalog, (repos.id, True, None, 'Repository', repos.title, None, None, False, False)) + for repos in self.catalog_plugins.get_repositories(catalogs): + repo_line = store.append(catalog, (repos.id, True, None, 'Repository', repos.title, None, None, False, False)) plugin_collections = self.catalog_plugins.get_collection(catalogs, repos.id) - client_plugins = list(plugin_collections) - client_plugins.sort() - for plugins in client_plugins: - installed = False - enabled = False - if plugin_collections[plugins]['name'] in self.config['plugins.installed']: - if repos.id == self.config['plugins.installed'][plugin_collections[plugins]['name']][1]: - installed = True - enabled = True if plugin_collections[plugins]['name'] in self.config['plugins.enabled'] else False - store.append(repo, ( - plugins, - enabled, - installed, - 'Plugin', - plugin_collections[plugins]['title'], - 'Yes' if self.catalog_plugins.is_compatible(catalogs, repos.id, plugins) else 'No', - self.compare_plugin_versions(plugin_collections[plugins]['name'], plugin_collections[plugins]['version']), - True, - True - )) + self._add_plugins_to_tree(catalogs, repos, store, repo_line, plugin_collections) else: + if not self.config['plugins.installed']: + return for catalog_id in self.catalog_plugins.get_cache_catalog_ids(): catalog_line = store.append(None, (catalog_id, True, None, 'Catalog (offline)', catalog_id, None, None, False, False)) - for repo in self.catalog_plugins.get_cache_repos(catalog_id): + for repo in self.catalog_plugins.get_cache_collections(catalog_id): repo_line = store.append(catalog_line, (repo.id, True, None, 'Repository (offline)', repo.title, None, None, False, False)) - for plugin in self.config['plugins.installed']: - if self.config['plugins.installed'][plugin][0] != catalog_id: - continue - if self.config['plugins.installed'][plugin][1] != repo.id: - continue - store.append(repo_line, ( - plugin, - True if plugin in self.config['plugins.enabled'] else False, - True, - 'Plugin', - pm[plugin].title, - 'Yes' if pm[plugin].is_compatible else 'No', - pm[plugin].version, - True, - False - )) + self._add_plugins_offline(catalog_id, repo.id, store, repo_line) + + def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): + client_plugins = list(plugin_list) + for plugins in client_plugins: + installed = False + enabled = False + if plugin_list[plugins]['name'] in self.config['plugins.installed']: + if repo.id == self.config['plugins.installed'][plugin_list[plugins]['name']][1]: + installed = True + enabled = True if plugin_list[plugins]['name'] in self.config['plugins.enabled'] else False + store.append(parent, ( + plugins, + enabled, + installed, + 'Plugin', + plugin_list[plugins]['title'], + 'Yes' if self.catalog_plugins.is_compatible(catalog, repo.id, plugins) else 'No', + self.compare_plugin_versions(plugin_list[plugins]['name'], plugin_list[plugins]['version']), + True, + True + )) + + def _add_plugins_offline(self, catalog_id, repo_id, store, parent): + for plugin in self.config['plugins.installed']: + if self.config['plugins.installed'][plugin][0] != catalog_id: + continue + if self.config['plugins.installed'][plugin][1] != repo_id: + continue + store.append(parent, ( + plugin, + True if plugin in self.config['plugins.enabled'] else False, + True, + 'Plugin', + self.application.plugin_manager[plugin].title, + 'Yes' if self.application.plugin_manager[plugin].is_compatible else 'No', + self.application.plugin_manager[plugin].version, + True, + False + )) def compare_plugin_versions(self, plugin_name, plugin_version): if plugin_name not in self.application.plugin_manager: @@ -244,10 +268,19 @@ def signal_label_activate_link(self, _, uri): def signal_eventbox_button_press(self, widget, event): if not (event.type == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY): return - name = self._last_plugin_selected - if name is None: + if not self._last_plugin_selected: + return + named_plugin = self._named_model(*self._last_plugin_selected) + plugin_id = named_plugin.id + if plugin_id is None: return - klass = self.application.plugin_manager[name] + if plugin_id in self.application.plugin_manager: + klass = self.application.plugin_manager[plugin_id] + compatibility_details = list(klass.compatibility) + else: + repo_model, catalog_model = self._get_plugin_model_parents(self._last_plugin_selected) + compatibility_details = list(self.catalog_plugins.compatibility(catalog_model.id, repo_model.id, named_plugin.id)) + popover = Gtk.Popover() popover.set_relative_to(self.gobjects['label_plugin_info_for_compatible']) grid = Gtk.Grid() @@ -257,7 +290,6 @@ def signal_eventbox_button_press(self, widget, event): grid.insert_column(0) grid.set_column_spacing(3) - compatibility_details = list(klass.compatibility) compatibility_details.insert(0, ('Type', 'Value', 'Met')) row = 0 for row, req in enumerate(compatibility_details): @@ -294,58 +326,57 @@ def signal_popup_menu_activate_reload(self, _): selected_plugin = model[tree_paths[0]][0] for tree_iter in gui_utilities.gtk_treeview_selection_iterate(treeview): - name = self._model[tree_iter][0] # pylint: disable=unsubscriptable-object - enabled = name in pm.enabled_plugins - pm.unload(name) + if self._model_item(tree_iter, 'type') != 'Plugin': + continue + plugin_id = self._model_item(tree_iter, 'id') + enabled = plugin_id in pm.enabled_plugins + pm.unload(plugin_id) try: - klass = pm.load(name, reload_module=True) + klass = pm.load(plugin_id, reload_module=True) except Exception as error: - self._on_plugin_load_error(name, error) - if name == selected_plugin: - self._set_plugin_info(name) - self._model[tree_iter][2] = "{0} (Reload Failed)".format(name) # pylint: disable=unsubscriptable-object + self._on_plugin_load_error(plugin_id, error) + if plugin_id == selected_plugin: + self._set_plugin_info(plugin_id) + self._set_model_item(tree_iter, 'title', "{0} (Reload Failed)".format(plugin_id)) continue - if name in self._module_errors: - del self._module_errors[name] - self._model[tree_iter][2] = klass.title # pylint: disable=unsubscriptable-object - if name == selected_plugin: - self._set_plugin_info(name) + if plugin_id in self._module_errors: + del self._module_errors[plugin_id] + self._set_model_item(tree_iter, 'title', klass.title) + if plugin_id == selected_plugin: + self._set_plugin_info(plugin_id) if enabled: - pm.enable(name) + pm.enable(plugin_id) def signal_renderer_toggled_enable(self, _, path): pm = self.application.plugin_manager - if self._model[path][3] != 'Plugin': + if self._model_item(path, 'type') != 'Plugin': return - plugin_model = self._model[path] - name = self._model[path][0] # pylint: disable=unsubscriptable-object - if name not in pm.loaded_plugins: + if self._model_item(path, 'id') not in pm.loaded_plugins: return if self._model[path].parent: - installed_plugin_info = self.config['plugins.installed'][name] - model_repo, model_cat = self._get_plugin_model_parents(self._model[path]) - if model_repo[0] != installed_plugin_info[1] or model_cat[0] != installed_plugin_info[0]: + installed_plugin_info = self.config['plugins.installed'][self._model_item(path, 'id')] + repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) + if repo_model.id != installed_plugin_info[1] or catalog_model.id != installed_plugin_info[0]: return - if name in self._module_errors: + if self._model_item(path, 'id') in self._module_errors: gui_utilities.show_dialog_error('Can Not Enable Plugin', self.window, 'Can not enable a plugin which failed to load.') return - if self._model[path][1]: # pylint: disable=unsubscriptable-object - self._disable_plugin(plugin_model) + if self._model_item(path, 'enabled'): + self._disable_plugin(path) else: - if not pm.loaded_plugins[name].is_compatible: + if not pm.loaded_plugins[self._model_item(path, 'id')].is_compatible: gui_utilities.show_dialog_error('Incompatible Plugin', self.window, 'This plugin is not compatible.') return - if not pm.enable(name): + if not pm.enable(self._model_item(path, 'id')): return - self._model[path][1] = True # pylint: disable=unsubscriptable-object - self.config['plugins.enabled'].append(name) + self._set_model_item(path, 'enabled', True) + self.config['plugins.enabled'].append(self._model_item(path, 'id')) def signal_renderer_toggled_install(self, _, path): - plugin_model = self._model[path] - repo_model, catalog_model = self._get_plugin_model_parents(plugin_model) - plugin_collection = self.catalog_plugins.get_collection(catalog_model[0], repo_model[0]) - if plugin_model[2]: - if plugin_model[1]: + repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) + plugin_collection = self.catalog_plugins.get_collection(catalog_model.id, repo_model.id) + if self._model_item(path, 'installed'): + if self._model_item(path, 'enabled'): response = gui_utilities.show_dialog_yes_no( 'Plugin is enabled', self.window, @@ -353,86 +384,122 @@ def signal_renderer_toggled_install(self, _, path): ) if not response: return - self._disable_plugin(plugin_model) - self._uninstall_plugin(plugin_collection, plugin_model) - self.logger.info("uninstalled plugin {}".format(plugin_model[0])) + self._disable_plugin(path) + self._uninstall_plugin(plugin_collection, path) + self.logger.info("uninstalled plugin {}".format(self._model_item(path, 'id'))) else: - if plugin_model[0] in self.config['plugins.installed']: - installed_plugin_info = self.config['plugins.installed'][plugin_model[0]] - if installed_plugin_info != [catalog_model[0], repo_model[0]]: + if self._model_item(path, 'id') in self.config['plugins.installed']: + installed_plugin_info = self.config['plugins.installed'][self._model_item(path, 'id')] + if installed_plugin_info != [catalog_model.id, repo_model.id]: + window_question = "A plugin with this name is already installed from Catalog: {} Repo: {}\nDo you want to replace it with this one?" response = gui_utilities.show_dialog_yes_no( 'Plugin installed from another source', self.window, - "A plugin with this name is already installed from Catalog: {} Repo: {}\nDo you want to replace it with this one?".format(installed_plugin_info[0], installed_plugin_info[1]) + window_question.format(installed_plugin_info[0], installed_plugin_info[1]) ) if not response: return - if not self._remove_matching_plugin(plugin_model[0], installed_plugin_info): - self.logger.warning('failed to uninstall plugin {}'.format(plugin_model[0])) + if not self._remove_matching_plugin(path, installed_plugin_info): + self.logger.warning('failed to uninstall plugin {}'.format(self._model_item(path, 'id'))) return - self.catalog_plugins.install_plugin(catalog_model[0], repo_model[0], plugin_model[0], DEFAULT_PLUGIN_PATH) - self.config['plugins.installed'][plugin_model[0]] = [catalog_model[0], repo_model[0]] - plugin_model[2] = True - plugin_model[6] = self.catalog_plugins.get_collection(catalog_model[0], repo_model[0])[plugin_model[0]]['version'] - self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(plugin_model[0], catalog_model[0], repo_model[0])) + self.catalog_plugins.install_plugin( + catalog_model.id, + repo_model.id, + self._model_item(path, 'id'), + DEFAULT_PLUGIN_PATH + ) + self.config['plugins.installed'][self._model_item(path, 'id')] = [catalog_model.id, repo_model.id] + self._set_model_item(path, 'installed', True) + self._set_model_item( + path, + 'version', + self.catalog_plugins.get_collection(catalog_model.id, repo_model.id)[self._model_item(path, 'id')]['version'] + ) + self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(self._model_item(path, 'id'), catalog_model.id, repo_model.id)) self.application.plugin_manager.load_all(on_error=self._on_plugin_load_error) - def _disable_plugin(self, plugin_model): - self.application.plugin_manager.disable(plugin_model[0]) - self.config['plugins.enabled'].remove(plugin_model[0]) - plugin_model[1] = False + def _disable_plugin(self, path, model_path=True): + if not model_path: + named_plugin = self._named_model(*path) + self.application.plugin_manager.disable(named_plugin.id) + self.config['plugins.enabled'].remove(named_plugin.id) + path[self._named_model._fields.index('enabled')] = False + else: + plugin_id = self._model_item(path, 'id') + self.application.plugin_manager.disable(plugin_id) + self.config['plugins.enabled'].remove(plugin_id) + self._set_model_item(path, 'enabled', False) - def _remove_matching_plugin(self, plugin_name, installed_plugin_info): + def _remove_matching_plugin(self, path, installed_plugin_info): + plugin_id = self._model_item(path, 'id') for catalog_model in self._model: - if catalog_model[0] != installed_plugin_info[0]: + if self._named_model(*catalog_model).id != installed_plugin_info[0]: continue for repo_model in catalog_model.iterchildren(): - if repo_model[0] != installed_plugin_info[1]: + if self._named_model(*repo_model).id != installed_plugin_info[1]: continue for plugin_model in repo_model.iterchildren(): - if plugin_model[0] != plugin_name: + named_model = self._named_model(*plugin_model) + if named_model.id != plugin_id: continue - if plugin_model[1]: - self._disable_plugin(plugin_model) - self._uninstall_plugin(self.catalog_plugins.get_collection(installed_plugin_info[0], installed_plugin_info[1]), plugin_model) + if named_model.enabled: + self._disable_plugin(plugin_model, model_path=False) + self._uninstall_plugin( + self.catalog_plugins.get_collection( + installed_plugin_info[0], + installed_plugin_info[1] + ), + plugin_model, + is_path=False + ) return True def _get_plugin_model_parents(self, plugin_model): - return plugin_model.parent, plugin_model.parent.parent + return self._named_model(*plugin_model.parent), self._named_model(*plugin_model.parent.parent) - def _uninstall_plugin(self, plugin_collection, plugin_model): - for files in plugin_collection[plugin_model[0]]['files']: + def _uninstall_plugin(self, plugin_collection, path, is_path=True): + if is_path: + plugin_id = self._model_item(path, 'id') + else: + plugin_id = self._named_model(*path).id + for files in plugin_collection[plugin_id]['files']: file_name = files[0] if os.path.isfile(os.path.join(DEFAULT_PLUGIN_PATH, file_name)): os.remove(os.path.join(DEFAULT_PLUGIN_PATH, file_name)) - del self.config['plugins.installed'][plugin_model[0]] - plugin_model[2] = False + self.application.plugin_manager.unload(plugin_id) + del self.config['plugins.installed'][plugin_id] + if is_path: + self._set_model_item(path, 'installed', False) + else: + path[self._named_model._fields.index('installed')] = False def _set_plugin_info(self, model_instance): + named_model = self._named_model(*model_instance) stack = self.gobjects['stack_plugin_info'] textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() buf.delete(buf.get_start_iter(), buf.get_end_iter()) - name = model_instance[0] - if model_instance[3] != 'Plugin': + model_id = named_model.id + if named_model.type != 'Plugin': stack.set_visible_child(textview) self._set_non_plugin_info(model_instance) return - if name in self._module_errors: + if model_id in self._module_errors: stack.set_visible_child(textview) - self._set_plugin_info_error(name) + self._set_plugin_info_error(model_id) else: stack.set_visible_child(self.gobjects['grid_plugin_info']) self._set_plugin_info_details(model_instance) def _set_non_plugin_info(self, model_instance): + named_model = self._named_model(*model_instance) textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() text = '' - if model_instance[3] == 'Catalog': - instance_information = self.catalog_plugins.catalogs[model_instance[0]] + if named_model.type == 'Catalog': + instance_information = self.catalog_plugins.catalogs[named_model.id] else: - instance_information = self.catalog_plugins.get_repo(model_instance.parent[0], model_instance[0]) + instance_information = self.catalog_plugins.get_repository(self._named_model(*model_instance.parent).id, named_model.id) if 'title' in dir(instance_information): text += "Repository: {}\n".format(instance_information.title if instance_information.title else instance_information.id) @@ -450,11 +517,12 @@ def _set_non_plugin_info(self, model_instance): buf.insert(buf.get_end_iter(), "{0}\n".format(text), -1) def _set_plugin_info_details(self, plugin_model): - name = plugin_model[0] + named_model = self._named_model(*plugin_model) + model_id = named_model.id pm = self.application.plugin_manager - self._last_plugin_selected = name - if name in pm.loaded_plugins: - klass = pm.loaded_plugins[name] + self._last_plugin_selected = plugin_model + if model_id in pm.loaded_plugins: + klass = pm.loaded_plugins[model_id] self.gobjects['label_plugin_info_title'].set_text(klass.title) self.gobjects['label_plugin_info_compatible'].set_text('Yes' if klass.is_compatible else 'No') self.gobjects['label_plugin_info_version'].set_text(klass.version) @@ -469,12 +537,14 @@ def _set_plugin_info_details(self, plugin_model): self.gobjects['label_plugin_info_description'].set_text(klass.description) else: repo_model, catalog_model = self._get_plugin_model_parents(plugin_model) - for repo in self.catalog_plugins.get_repos(catalog_model[0]): - if repo.id != repo_model[0]: + for repo in self.catalog_plugins.get_repositories(catalog_model.id): + if repo.id != repo_model.id: continue - plugin = repo.collections['plugins/client'][plugin_model[0]] + plugin = repo.collections['plugins/client'][named_model.id] self.gobjects['label_plugin_info_title'].set_text(plugin['title']) - self.gobjects['label_plugin_info_compatible'].set_text('Yes' if self.catalog_plugins.is_compatible(catalog_model[0], repo_model[0], plugin_model[0]) else 'No') + self.gobjects['label_plugin_info_compatible'].set_text( + 'Yes' if self.catalog_plugins.is_compatible(catalog_model.id, repo_model.id, named_model.id) else 'No' + ) self.gobjects['label_plugin_info_version'].set_text(plugin['version']) self.gobjects['label_plugin_info_authors'].set_text('\n'.join(plugin['authors'])) label_homepage = self.gobjects['label_plugin_info_homepage'] @@ -487,9 +557,9 @@ def _set_plugin_info_details(self, plugin_model): self.gobjects['label_plugin_info_description'].set_text(plugin['description']) def _set_plugin_info_error(self, model_instance): - name = model_instance[0] + id_ = self._named_model(*model_instance).id textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() - exc, formatted_exc = self._module_errors[name] + exc, formatted_exc = self._module_errors[id_] buf.insert(buf.get_end_iter(), "{0!r}\n\n".format(exc), -1) buf.insert(buf.get_end_iter(), ''.join(formatted_exc), -1) diff --git a/king_phisher/plugins.py b/king_phisher/plugins.py index c5148e45..972a3d2c 100644 --- a/king_phisher/plugins.py +++ b/king_phisher/plugins.py @@ -29,13 +29,18 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # - +import collections +import copy import distutils.version import inspect import logging +import re +import sys import textwrap import threading +import smoke_zephyr.requirements + from king_phisher import errors from king_phisher import its from king_phisher import version @@ -48,6 +53,11 @@ import importlib _reload = importlib.reload +if sys.version_info[:3] >= (3, 3, 0): + _Mapping = collections.abc.Mapping +else: + _Mapping = collections.Mapping + StrictVersion = distutils.version.StrictVersion def _recursive_reload(package, package_name, completed): @@ -115,6 +125,58 @@ class OptionString(OptionBase): """A plugin option which is represented with a string value.""" pass +class Requirements(_Mapping): + _package_regex = re.compile(r'^(?P[\w\-]+)(([<>=]=)(\d+(\.\d+)*))?$') + def __init__(self, items): + """ + :param dict items: The items that are members of this collection, keyed by their name. + """ + # call dict here to allow items to be a two dimensional array suitable for passing to dict + self._storage = dict(items) + + def __repr__(self): + return "<{0} is_compatible={1!r} >".format(self.__class__.__name__, self.is_compatible) + + def __getitem__(self, key): + return self._storage[key] + + def __iter__(self): + return iter(self._storage) + + def __len__(self): + return len(self._storage) + + @property + def is_compatible(self): + for req_type, req_details, req_met in self.compatibility_iter(): + if not req_met: + return False + return True + + def compatibility_iter(self): + if 'minimum-version' in self._storage: + StrictVersion = distutils.version.StrictVersion + yield ('King Phisher Version', self._storage['minimum-version'], StrictVersion(self._storage['minimum-version']) <= StrictVersion(version.distutils_version)) + if 'packages' in self._storage: + packages = self._storage['packages'] + try: + missing_packages = smoke_zephyr.requirements.check_requirements(self._storage['packages']) + except ValueError: + missing_packages = self._storage['packages'] + for package in packages: + #if not self._package_regex.match(package): + # continue + if self._package_regex.match(package): + package = self._package_regex.match(package).group('name') + yield ('Required Package', package, package not in missing_packages) + + @property + def compatibility(self): + return tuple(self.compatibility_iter()) + + def to_dict(self): + return copy.deepcopy(self._storage) + class PluginBaseMeta(type): """ The meta class for :py:class:`.PluginBase` which provides additional class @@ -422,7 +484,7 @@ def load_module(self, name, reload_module=False): recursive_reload(module) return module - def PluginBaseMeta(self, name): + def unload(self, name): """ Unload a plugin from memory. If the specified plugin is currently enabled, it will first be disabled before being unloaded. If the plugin From 465482d2492dba37054e6b6ee9c380a55aa848bd Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Wed, 25 Oct 2017 15:02:27 -0400 Subject: [PATCH 12/38] Final issues based on feedback --- king_phisher/catalog.py | 10 ++-- king_phisher/client/plugins.py | 58 +++++++++---------- king_phisher/client/windows/plugin_manager.py | 31 ++++------ 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index 28cb4ebc..86961524 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -354,29 +354,31 @@ class CatalogManager(object): """ def __init__(self, url_catalog=None): self.catalogs = {} - if url_catalog: self.add_catalog_url(url_catalog) def catalog_ids(self): """ The key names of the catalogs in the manager + :return: the catalogs ids in CatalogManager - :rtype: list + :rtype: dict_keys """ return self.catalogs.keys() def get_repositories(self, catalog_id): """ Returns a list of repositories from the requested catalog + :param str catalog_id: The name of the catalog in which to get names of repositories from - :return: list + :return: tuple """ - return [repository for repository in self.catalogs[catalog_id].repositories] + return self.catalogs[catalog_id].repositories def get_repository(self, catalog_id, repo_id): """ Returns the requested repository instance + :param str catalog_id: The name of the catalog the repo belongs to :param str repo_id: The id of the repository requested. :return: The repository instance diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 25eabf4f..76947e88 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -31,30 +31,28 @@ # import collections -import json import os import sys import tempfile import weakref -from king_phisher import plugins from king_phisher import catalog +from king_phisher import plugins +from king_phisher.client import application from king_phisher.client import gui_utilities from king_phisher.client import mailer from king_phisher.client.widget import extras from king_phisher.plugins import Requirements +from king_phisher.serializers import JSON from gi.repository import Gtk import jinja2.exceptions -from gi.repository import GLib if sys.version_info[:3] >= (3, 3, 0): _MutableMapping = collections.abc.MutableMapping else: _MutableMapping = collections.MutableMapping -DEFAULT_CONFIG_PATH = os.path.join(GLib.get_user_config_dir(), 'king-phisher') - def _split_menu_path(menu_path): menu_path = [path_item.strip() for path_item in menu_path.split('>')] menu_path = [path_item for path_item in menu_path if path_item] @@ -563,16 +561,16 @@ class CatalogCacheManager(object): def __init__(self, cache_file): self._cache_dict = {} self._data = {} - self._CatalogCacheEntry = collections.namedtuple('catalog', ['id', 'collections']) - self._CollectionCacheEntry = collections.namedtuple('collection', ['id', 'title', 'url', 'types']) + self._CatalogCacheEntry = collections.namedtuple('catalog', ['id', 'repositories']) + self._RepositoryCacheEntry = collections.namedtuple('repositories', ['id', 'title', 'url', 'collections']) + self._cache_file = cache_file - if os.path.isfile(cache_file): + if os.path.isfile(self._cache_file): with open(cache_file) as file_h: try: - self._cache_dict = json.load(file_h) + self._cache_dict = JSON.loads(file_h.read()) except ValueError: self._cache_dict = {} - self._cache_file = cache_file if not self._cache_dict or 'catalogs' not in self._cache_dict: self._cache_dict['catalogs'] = {} @@ -581,19 +579,19 @@ def __init__(self, cache_file): for catalog_ in cache_cat: self[catalog_] = self._CatalogCacheEntry( cache_cat[catalog_]['id'], - self._tuple_collections(cache_cat[catalog_]['collections']) + self._tuple_repositories(cache_cat[catalog_]['repositories']) ) - def _tuple_collections(self, list_collections): - collections_ = [] - for collection_ in list_collections: - collections_.append(self._CollectionCacheEntry( - collection_['id'], - collection_['title'], - collection_['url'], - collection_['types'] + def _tuple_repositories(self, list_repositories): + repositories = [] + for repository in list_repositories: + repositories.append(self._RepositoryCacheEntry( + repository['id'], + repository['title'], + repository['url'], + repository['collections'] )) - return collections_ + return repositories def __setitem__(self, key, value): self._data[key] = value @@ -608,13 +606,12 @@ def __len__(self): return len(self._data) def __iter__(self): - for key in self._data.keys(): - yield key + return iter(self._data) - def add_catalog_cache(self, cat_id, collections_): + def add_catalog_cache(self, cat_id, repositories): self[cat_id] = self._CatalogCacheEntry( cat_id, - self._tuple_collections(collections_) + self._tuple_repositories(repositories) ) def to_dict(self): @@ -622,24 +619,23 @@ def to_dict(self): for key in self: cache[key] = {} cache[key]['id'] = self[key].id - collections_ = [] - for collection_ in self[key].collections: - collections_.append(dict(collection_._asdict())) - cache[key]['collections'] = collections_ + repositories = [] + for repository in self[key].repositories: + repositories.append(dict(repository._asdict())) + cache[key]['repositories'] = repositories return cache def save(self): self._cache_dict['catalogs'] = self.to_dict() with open(self._cache_file, 'w+') as file_h: - json.dump(self._cache_dict, file_h, sort_keys=True, indent=4) + file_h.write(JSON.dumps(self._cache_dict)) class ClientCatalogManager(catalog.CatalogManager): """ Base manager for handling Catalogs """ - def __init__(self, manager_type=None, *args, **kwargs): - self._catalog_cache = CatalogCacheManager(os.path.join(DEFAULT_CONFIG_PATH, 'cache.json')) + self._catalog_cache = CatalogCacheManager(os.path.join(application.USER_DATA_PATH, 'cache.json')) super(ClientCatalogManager, self).__init__(*args, **kwargs) self.manager_type = 'plugins/client' if manager_type: diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index c522e0d3..1e0e38d6 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -37,17 +37,15 @@ from collections import namedtuple from king_phisher import utilities +from king_phisher.client import application from king_phisher.client import gui_utilities from king_phisher.client.widget import managers from king_phisher.client.plugins import ClientCatalogManager from gi.repository import Gdk from gi.repository import Gtk -from gi.repository import GLib __all__ = ('PluginManagerWindow',) -DEFAULT_PLUGIN_PATH = os.path.join(GLib.get_user_config_dir(), 'king-phisher', 'plugins') - class PluginManagerWindow(gui_utilities.GladeGObject): """ The window which allows the user to selectively enable and disable plugins @@ -119,8 +117,9 @@ def __init__(self, *args, **kwargs): ] ) self._model = Gtk.TreeStore(str, bool, bool, str, str, str, str, bool, bool) - self._model.set_sort_column_id(2, Gtk.SortType.ASCENDING) + self._model.set_sort_column_id(2, Gtk.SortType.DESCENDING) treeview.set_model(self._model) + self.DEFAULT_PLUGIN_PATH = os.path.join(application.USER_DATA_PATH, 'plugins') self.catalog_plugins = ClientCatalogManager(url_catalog='https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json') self.logger.warning("failed to connect to catalog server") @@ -406,7 +405,7 @@ def signal_renderer_toggled_install(self, _, path): catalog_model.id, repo_model.id, self._model_item(path, 'id'), - DEFAULT_PLUGIN_PATH + self.DEFAULT_PLUGIN_PATH ) self.config['plugins.installed'][self._model_item(path, 'id')] = [catalog_model.id, repo_model.id] self._set_model_item(path, 'installed', True) @@ -464,8 +463,8 @@ def _uninstall_plugin(self, plugin_collection, path, is_path=True): plugin_id = self._named_model(*path).id for files in plugin_collection[plugin_id]['files']: file_name = files[0] - if os.path.isfile(os.path.join(DEFAULT_PLUGIN_PATH, file_name)): - os.remove(os.path.join(DEFAULT_PLUGIN_PATH, file_name)) + if os.path.isfile(os.path.join(self.DEFAULT_PLUGIN_PATH, file_name)): + os.remove(os.path.join(self.DEFAULT_PLUGIN_PATH, file_name)) self.application.plugin_manager.unload(plugin_id) del self.config['plugins.installed'][plugin_id] if is_path: @@ -501,18 +500,12 @@ def _set_non_plugin_info(self, model_instance): else: instance_information = self.catalog_plugins.get_repository(self._named_model(*model_instance.parent).id, named_model.id) - if 'title' in dir(instance_information): - text += "Repository: {}\n".format(instance_information.title if instance_information.title else instance_information.id) - else: - text += "Catalog: {}\n".format(instance_information.id) - - if 'maintainers' in dir(instance_information): - if instance_information.maintainers: - text += 'Maintainer: ' + '\nMaintainer: '.join(instance_information.maintainers) + '\n' - - if 'description' in dir(instance_information): - if instance_information.description: - text += instance_information.description + '\n' + text += "{0}\n".format(named_model.type) + text += "Title: {0}\n".format(getattr(instance_information, 'title', 'None')) + text += "Id: {0}\n".format(getattr(instance_information, 'id', 'None')) + text += "Description: {}\n".format(getattr(instance_information, 'description', 'None')) + if hasattr(instance_information, 'maintainers'): + text += 'Maintainer: ' + '\nMaintainer: '.join(getattr(instance_information, 'maintainers', ['None'])) + '\n' buf.insert(buf.get_end_iter(), "{0}\n".format(text), -1) From a2f69abaa126ef7ba7bc97743fa4efb87057084c Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Fri, 27 Oct 2017 15:19:27 -0400 Subject: [PATCH 13/38] Added utilties.Thread and threaded plugin manager --- king_phisher/client/plugins.py | 5 +- king_phisher/client/windows/plugin_manager.py | 165 ++++++++++++------ king_phisher/plugins.py | 21 ++- king_phisher/utilities.py | 57 ++++++ 4 files changed, 186 insertions(+), 62 deletions(-) diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 76947e88..5d53898b 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -697,6 +697,5 @@ def get_cache_catalog_ids(self): for item in self._catalog_cache: yield item - def get_cache_collections(self, catalog_id): - for collections_ in self._catalog_cache[catalog_id].collections: - yield collections_ + def get_cache_repositories(self, catalog_id): + return iter(self._catalog_cache[catalog_id].repositories) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 1e0e38d6..5f6ce5d3 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -77,71 +77,100 @@ class PluginManagerWindow(gui_utilities.GladeGObject): def __init__(self, *args, **kwargs): super(PluginManagerWindow, self).__init__(*args, **kwargs) treeview = self.gobjects['treeview_plugins'] - self._last_plugin_selected = None + self.status_bar = self.gobjects['statusbar'] self._module_errors = {} tvm = managers.TreeViewManager( treeview, - cb_refresh=self.load_plugins + cb_refresh=self._load_plugins ) toggle_renderer_enable = Gtk.CellRendererToggle() toggle_renderer_enable.connect('toggled', self.signal_renderer_toggled_enable) toggle_renderer_install = Gtk.CellRendererToggle() toggle_renderer_install.connect('toggled', self.signal_renderer_toggled_install) tvm.set_column_titles( - ['Enabled', 'Installed', 'Type', 'Title', 'Compatible', 'Version'], + ['Installed', 'Enabled', 'Title', 'Compatible', 'Version'], column_offset=1, renderers=[ - toggle_renderer_enable, toggle_renderer_install, - Gtk.CellRendererText(), + toggle_renderer_enable, Gtk.CellRendererText(), Gtk.CellRendererText(), Gtk.CellRendererText() ] ) tvm.column_views['Enabled'].set_cell_data_func(toggle_renderer_enable, self._toggle_cell_data_func) - tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'visible', 7) - tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 8) + tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'visible', 6) + tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'sensitive', 7) + tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 7) + tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'sensitive', 8) self._named_model = namedtuple( 'model_row', [ 'id', - 'enabled', 'installed', - 'type', + 'enabled', 'title', 'compatibility', 'version', 'visible_enabled', 'visible_installed', + 'installed_sensitive', + 'type' ] ) - self._model = Gtk.TreeStore(str, bool, bool, str, str, str, str, bool, bool) - self._model.set_sort_column_id(2, Gtk.SortType.DESCENDING) + self._model = Gtk.TreeStore(str, bool, bool, str, str, str, bool, bool, bool, str) + self._model.set_sort_column_id(1, Gtk.SortType.DESCENDING) treeview.set_model(self._model) - self.DEFAULT_PLUGIN_PATH = os.path.join(application.USER_DATA_PATH, 'plugins') - - self.catalog_plugins = ClientCatalogManager(url_catalog='https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json') - self.logger.warning("failed to connect to catalog server") - - self.load_plugins() - + self.plugin_path = os.path.join(application.USER_DATA_PATH, 'plugins') + if 'catalogs' not in self.config: + self.config['catalogs'] = ['https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json'] + self.catalog_list = self.config['catalogs'] + self.load_thread = utilities.Thread(target=self._load_catalogs) + self.load_thread.start() self.popup_menu = tvm.get_popup_menu() self.popup_menu.append(Gtk.SeparatorMenuItem()) menu_item = Gtk.MenuItem.new_with_label('Reload') menu_item.connect('activate', self.signal_popup_menu_activate_reload) self.popup_menu.append(menu_item) menu_item_reload_all = Gtk.MenuItem.new_with_label('Reload All') - menu_item_reload_all.connect('activate', self.signal_popup_menu_activate_relaod_all) + menu_item_reload_all.connect('activate', self.signal_popup_menu_activate_reload_all) self.popup_menu.append(menu_item_reload_all) self.popup_menu.show_all() - + self._update_status_bar('Loading: ') self.window.show() + selection = treeview.get_selection() selection.unselect_all() paned = self.gobjects['paned_plugins'] self._paned_offset = paned.get_allocation().height - paned.get_position() + def _treeview_unselect(self): + treeview = self.gobjects['treeview_plugins'] + treeview.get_selection().unselect_all() + self._update_status_bar('Finished Loading Plugin Store', idle=True) + + def signal_window_show(self, _): + pass + + def _load_catalogs(self): + self._update_status_bar('Loading: Downloading Catalogs', idle=True) + self.catalog_plugins = ClientCatalogManager() + for catalog in self.catalog_list: + self.logger.debug("downloading catalog {}".format(catalog)) + self._update_status_bar("Loading: Downloading Catalog: {}".format(catalog)) + self.catalog_plugins.add_catalog_url(catalog) + self._load_plugins() + + def __update_status_bar(self, string_to_set): + self.status_bar.pop(0) + self.status_bar.push(0, string_to_set) + + def _update_status_bar(self, string_to_set, idle=False): + if idle: + gui_utilities.glib_idle_add_once(self.__update_status_bar, string_to_set) + else: + self.__update_status_bar(string_to_set) + def _model_item(self, model_path, item): named_row = self._named_model(*self._model[model_path]) return getattr(named_row, item) @@ -158,53 +187,67 @@ def _toggle_cell_data_func(self, column, cell, model, tree_iter, _): else: cell.set_property('inconsistent', False) - def load_plugins(self): + def _store_append(self, store, parent, model): + return store.append(parent, model) + + def _load_plugins(self): """ Load the plugins which are available into the treeview to make them visible to the user. """ + self.logger.debug('loading plugins') + self._update_status_bar('Loading.... Plugins....', idle=True) store = self._model store.clear() pm = self.application.plugin_manager self._module_errors = {} pm.load_all(on_error=self._on_plugin_load_error) + model = ('local', None, True, '[Locally Installed]', None, None, False, False, False, 'Catalog') + catalog = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) for name, plugin in pm.loaded_plugins.items(): if name in self.config['plugins.installed']: continue - store.append(None, ( + model = ( plugin.name, - plugin.name in pm.enabled_plugins, True, - 'Plugin', + plugin.name in pm.enabled_plugins, plugin.title, 'Yes' if plugin.is_compatible else 'No', plugin.version, True, - False - )) - for name in self._module_errors.keys(): - store.append(( - name, + True, False, - "{0} (Load Failed)".format(name) - )) + 'Plugin' + ) + gui_utilities.glib_idle_add_once(self._store_append, store, catalog, model) + for name in self._module_errors.keys(): + model = (name, True, False, "{0} (Load Failed)".format(name), 'unknown', True, True, False, 'Plugin') + gui_utilities.glib_idle_add_once(self._store_append, store, catalog, model) + + self.logger.debug('loading catalog into plugin treeview') if self.catalog_plugins.catalog_ids(): for catalogs in self.catalog_plugins.catalog_ids(): - catalog = store.append(None, (catalogs, True, None, 'Catalog', catalogs, None, None, False, False)) + model = (catalogs, None, True, catalogs, None, None, False, False, False, 'Catalog') + catalog = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) for repos in self.catalog_plugins.get_repositories(catalogs): - repo_line = store.append(catalog, (repos.id, True, None, 'Repository', repos.title, None, None, False, False)) + model = (repos.id, None, True, repos.title, None, None, False, False, False, 'Repository') + repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog, model) plugin_collections = self.catalog_plugins.get_collection(catalogs, repos.id) self._add_plugins_to_tree(catalogs, repos, store, repo_line, plugin_collections) else: if not self.config['plugins.installed']: return for catalog_id in self.catalog_plugins.get_cache_catalog_ids(): - catalog_line = store.append(None, (catalog_id, True, None, 'Catalog (offline)', catalog_id, None, None, False, False)) - for repo in self.catalog_plugins.get_cache_collections(catalog_id): - repo_line = store.append(catalog_line, (repo.id, True, None, 'Repository (offline)', repo.title, None, None, False, False)) + model = (catalog_id, None, True, "{} (offline)".format(catalog_id), None, None, False, False, False, 'Catalog (offline)') + catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) + for repo in self.catalog_plugins.get_cache_repositories(catalog_id): + model = (repo.id, None, True, "{} (offline)".format(repo.title), None, None, False, False, False, 'Repository (offline)') + repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) self._add_plugins_offline(catalog_id, repo.id, store, repo_line) + gui_utilities.glib_idle_add_once(self._treeview_unselect) + def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): client_plugins = list(plugin_list) for plugins in client_plugins: @@ -214,17 +257,19 @@ def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): if repo.id == self.config['plugins.installed'][plugin_list[plugins]['name']][1]: installed = True enabled = True if plugin_list[plugins]['name'] in self.config['plugins.enabled'] else False - store.append(parent, ( + model = ( plugins, - enabled, installed, - 'Plugin', + enabled, plugin_list[plugins]['title'], 'Yes' if self.catalog_plugins.is_compatible(catalog, repo.id, plugins) else 'No', - self.compare_plugin_versions(plugin_list[plugins]['name'], plugin_list[plugins]['version']), + self._get_version_or_upgrade(plugin_list[plugins]['name'], plugin_list[plugins]['version']), True, - True - )) + True, + True, + 'Plugin' + ) + gui_utilities.glib_idle_add_once(self._store_append, store, parent, model) def _add_plugins_offline(self, catalog_id, repo_id, store, parent): for plugin in self.config['plugins.installed']: @@ -232,27 +277,30 @@ def _add_plugins_offline(self, catalog_id, repo_id, store, parent): continue if self.config['plugins.installed'][plugin][1] != repo_id: continue - store.append(parent, ( + model = ( plugin, - True if plugin in self.config['plugins.enabled'] else False, True, - 'Plugin', + True if plugin in self.config['plugins.enabled'] else False, self.application.plugin_manager[plugin].title, 'Yes' if self.application.plugin_manager[plugin].is_compatible else 'No', self.application.plugin_manager[plugin].version, True, - False - )) + True, + False, + 'Plugin' + ) + gui_utilities.glib_idle_add_once(self._store_append, store, parent, model) - def compare_plugin_versions(self, plugin_name, plugin_version): + def _get_version_or_upgrade(self, plugin_name, plugin_version): if plugin_name not in self.application.plugin_manager: return plugin_version if self.application.plugin_manager[plugin_name].version < plugin_version: return "Upgrade available" return self.application.plugin_manager[plugin_name].version - def signal_popup_menu_activate_relaod_all(self, _): - self.load_plugins() + def signal_popup_menu_activate_reload_all(self, _): + self.load_thread = utilities.Thread(target=self._load_plugins) + self.load_thread.start() def signal_destory(self, _): if self.catalog_plugins: @@ -405,7 +453,7 @@ def signal_renderer_toggled_install(self, _, path): catalog_model.id, repo_model.id, self._model_item(path, 'id'), - self.DEFAULT_PLUGIN_PATH + self.plugin_path ) self.config['plugins.installed'][self._model_item(path, 'id')] = [catalog_model.id, repo_model.id] self._set_model_item(path, 'installed', True) @@ -463,8 +511,8 @@ def _uninstall_plugin(self, plugin_collection, path, is_path=True): plugin_id = self._named_model(*path).id for files in plugin_collection[plugin_id]['files']: file_name = files[0] - if os.path.isfile(os.path.join(self.DEFAULT_PLUGIN_PATH, file_name)): - os.remove(os.path.join(self.DEFAULT_PLUGIN_PATH, file_name)) + if os.path.isfile(os.path.join(self.plugin_path, file_name)): + os.remove(os.path.join(self.plugin_path, file_name)) self.application.plugin_manager.unload(plugin_id) del self.config['plugins.installed'][plugin_id] if is_path: @@ -492,6 +540,9 @@ def _set_plugin_info(self, model_instance): def _set_non_plugin_info(self, model_instance): named_model = self._named_model(*model_instance) + if 'offline' in named_model.type or named_model.id == 'local': + self._set_non_plugin_offline_info(model_instance) + return textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() text = '' @@ -509,6 +560,14 @@ def _set_non_plugin_info(self, model_instance): buf.insert(buf.get_end_iter(), "{0}\n".format(text), -1) + def _set_non_plugin_offline_info(self, model_instance): + named_model = self._named_model(*model_instance) + textview = self.gobjects['textview_plugin_info'] + buf = textview.get_buffer() + text = '{0}\n'.format(named_model.type.split()[0]) + text += "Title: {0}".format(named_model.title) + buf.insert(buf.get_end_iter(), "{0}\n".format(text), -1) + def _set_plugin_info_details(self, plugin_model): named_model = self._named_model(*plugin_model) model_id = named_model.id diff --git a/king_phisher/plugins.py b/king_phisher/plugins.py index 972a3d2c..48cf3d07 100644 --- a/king_phisher/plugins.py +++ b/king_phisher/plugins.py @@ -159,17 +159,26 @@ def compatibility_iter(self): yield ('King Phisher Version', self._storage['minimum-version'], StrictVersion(self._storage['minimum-version']) <= StrictVersion(version.distutils_version)) if 'packages' in self._storage: packages = self._storage['packages'] - try: - missing_packages = smoke_zephyr.requirements.check_requirements(self._storage['packages']) - except ValueError: - missing_packages = self._storage['packages'] + missing_packages = self._check_for_missing_packages(self._storage['packages']) + for package in packages: - #if not self._package_regex.match(package): - # continue if self._package_regex.match(package): package = self._package_regex.match(package).group('name') yield ('Required Package', package, package not in missing_packages) + def _check_for_missing_packages(self, packages): + missing_packages = [] + for package in packages: + if package.startswith('gi.'): + if not importlib.util.find_spec(package): + missing_packages.append(package) + logging.info('During requirement check, gi subpackage was manually checked') + continue + package_check = smoke_zephyr.requirements.check_requirements([package]) + if package_check: + missing_packages.append(package_check[0]) + return missing_packages + @property def compatibility(self): return tuple(self.compatibility_iter()) diff --git a/king_phisher/utilities.py b/king_phisher/utilities.py index e4227ee8..6b2d1de9 100644 --- a/king_phisher/utilities.py +++ b/king_phisher/utilities.py @@ -46,6 +46,7 @@ import string import subprocess import sys +import threading from king_phisher import color from king_phisher import constants @@ -482,3 +483,59 @@ def validate_json_schema(data, schema_file_id): with open(file_path, 'r') as file_h: schema = json.load(file_h) jsonschema.validate(data, schema) + +class Thread(threading.Thread): + """ + King Phishers base threading class with two way event. + """ + + def __init__(self, *args, **kwargs): + super(Thread, self).__init__(*args, **kwargs) + self.logger = logging.getLogger('KingPhisher.Thread.{0}'.format(self.name)) + self.stop_flag = Event() + self.stop_flag.clear() + + def run(self): + self.logger.debug("thread {0} running in tid: 0x{1:x}".format(self.name, threading.current_thread().ident)) + super(Thread, self).run() + + def stop(self): + """ + Sets the flag to signal the threat to stop. + """ + self.stop_flag.set() + + def is_stopped(self): + """ + Check to see if the flag is set to stop the thread. + """ + return self.stop_flag.is_set() + +class Event(threading.Event): + __slots__ = ('__event',) + def __init__(self): + super(Event, self).__init__() + self.__event = threading.Event() + self.__event.set() + + def __repr__(self): + return "<{0} is_set={1!r} >".format(self.__class__.__name__, self.is_set()) + + def clear(self): + super(Event, self).clear() + self.__event.set() + + def is_clear(self): + return self.__event.is_set() + + def set(self): + self.__event.clear() + super(Event, self).set() + + def wait(self, timeout=None): + if super(Event, self).wait(timeout=timeout): + self.__event.clear() + + def wait_clear(self, timeout=None): + if self.__event.wait(timeout=timeout): + super(Event, self).set() From fb9086fa528e381169b7528ee2382926484489b2 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 30 Oct 2017 16:36:04 -0400 Subject: [PATCH 14/38] Refactoring of the plugin manager --- data/client/king_phisher/client_config.json | 3 + king_phisher/catalog.py | 39 +- king_phisher/client/plugins.py | 73 ++-- king_phisher/client/windows/plugin_manager.py | 336 ++++++++---------- king_phisher/plugins.py | 2 +- king_phisher/utilities.py | 5 +- 6 files changed, 193 insertions(+), 265 deletions(-) diff --git a/data/client/king_phisher/client_config.json b/data/client/king_phisher/client_config.json index 51b23423..25c10dc8 100644 --- a/data/client/king_phisher/client_config.json +++ b/data/client/king_phisher/client_config.json @@ -12,6 +12,9 @@ "filter.campaign.user": true, "filter.campaign.other_users": false, "plugins": {}, + "plugins.catalogs": [ + "https://raw.githubusercontent.com/securestate/king-phisher-plugins/master/catalog.json" + ], "plugins.enabled": [], "plugins.installed": {}, "rpc.serializer": null, diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index 86961524..1268f071 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -134,7 +134,7 @@ def __init__(self, data, keys=None): self.homepage = data.get('homepage') """The URL of the homepage for this repository if it was specified.""" self.id = data['id'] - """The unique id of this repository""" + """The unique identifier of this repository.""" self.title = data['title'] """The title string of this repository.""" self.url_base = data['url-base'] @@ -314,15 +314,15 @@ def __init__(self, data, keys=None): self.created = dateutil.parser.parse(data['created']) """The timestamp of when the remote data was generated.""" self.id = data['id'] - """The unique id of the catalog""" + """The unique identifier of this catalog.""" self.maintainers = tuple(maintainer['id'] for maintainer in data['maintainers']) """ A tuple containing the maintainers of the catalog and repositories. These are also the key identities that should be present for verifying the remote data. """ - self.repositories = tuple(Repository(repo, keys=self.security_keys) for repo in data['repositories']) - """A tuple of the :py:class:`.Repository` objects included in this catalog.""" + self.repositories = dict((repo['id'], Repository(repo, keys=self.security_keys)) for repo in data['repositories']) + """A dict of the :py:class:`.Repository` objects included in this catalog keyed by their id.""" self.logger.info("initialized catalog with {0:,} repositories".format(len(self.repositories))) @classmethod @@ -350,12 +350,13 @@ def from_url(cls, url, keys=None, encoding='utf-8'): class CatalogManager(object): """ - Base manager for handling multiple Catalogs + Base manager for handling multiple :py:class:`.Catalogs`. """ - def __init__(self, url_catalog=None): + logger = logging.getLogger('KingPhisher.Catalog.Manager') + def __init__(self, catalog_url=None): self.catalogs = {} - if url_catalog: - self.add_catalog_url(url_catalog) + if catalog_url: + self.add_catalog_url(catalog_url) def catalog_ids(self): """ @@ -368,33 +369,19 @@ def catalog_ids(self): def get_repositories(self, catalog_id): """ - Returns a list of repositories from the requested catalog + Returns repositories from the requested catalog. - :param str catalog_id: The name of the catalog in which to get names of repositories from + :param str catalog_id: The name of the catalog in which to get names of repositories from. :return: tuple """ - return self.catalogs[catalog_id].repositories - - def get_repository(self, catalog_id, repo_id): - """ - Returns the requested repository instance - - :param str catalog_id: The name of the catalog the repo belongs to - :param str repo_id: The id of the repository requested. - :return: The repository instance - :rtype:py:class:Repository - """ - for repo in self.catalogs[catalog_id].repositories: - if repo.id != repo_id: - continue - return repo + return tuple(self.catalogs[catalog_id].repositories.values()) def add_catalog_url(self, url): try: c = Catalog.from_url(url) self.catalogs[c.id] = c except Exception as error: - logging.warning("failed to load catalog from url {} due to {}".format(url, error)) + self.logger.warning("failed to load catalog from url {0} due to {1}".format(url, error), exc_info=True) def sign_item_files(local_path, signing_key, signing_key_id, repo_path=None): """ diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 5d53898b..0ee0701b 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -556,42 +556,34 @@ def __init__(self, path, application): class CatalogCacheManager(object): """ - Manager to handle cache information for catalogs + Manager to handle cache information for catalogs. """ + _CatalogCacheEntry = collections.namedtuple('catalog', ['id', 'repositories']) + _RepositoryCacheEntry = collections.namedtuple('repositories', ['id', 'title', 'url', 'collections']) def __init__(self, cache_file): self._cache_dict = {} self._data = {} - self._CatalogCacheEntry = collections.namedtuple('catalog', ['id', 'repositories']) - self._RepositoryCacheEntry = collections.namedtuple('repositories', ['id', 'title', 'url', 'collections']) self._cache_file = cache_file if os.path.isfile(self._cache_file): - with open(cache_file) as file_h: + with open(cache_file, 'r') as file_h: try: - self._cache_dict = JSON.loads(file_h.read()) + self._cache_dict = JSON.load(file_h) except ValueError: self._cache_dict = {} - if not self._cache_dict or 'catalogs' not in self._cache_dict: - self._cache_dict['catalogs'] = {} - else: + if self._cache_dict and 'catalogs' in self._cache_dict: cache_cat = self._cache_dict['catalogs'] for catalog_ in cache_cat: self[catalog_] = self._CatalogCacheEntry( cache_cat[catalog_]['id'], self._tuple_repositories(cache_cat[catalog_]['repositories']) ) + else: + self._cache_dict['catalogs'] = {} - def _tuple_repositories(self, list_repositories): - repositories = [] - for repository in list_repositories: - repositories.append(self._RepositoryCacheEntry( - repository['id'], - repository['title'], - repository['url'], - repository['collections'] - )) - return repositories + def _tuple_repositories(self, repositories): + return tuple(self._RepositoryCacheEntry(**repository) for repository in repositories) def __setitem__(self, key, value): self._data[key] = value @@ -608,38 +600,30 @@ def __len__(self): def __iter__(self): return iter(self._data) - def add_catalog_cache(self, cat_id, repositories): - self[cat_id] = self._CatalogCacheEntry( - cat_id, - self._tuple_repositories(repositories) - ) + def cache_catalog_repositories(self, cat_id, repositories): + self[cat_id] = self._CatalogCacheEntry(cat_id, self._tuple_repositories(repositories)) def to_dict(self): cache = {} for key in self: cache[key] = {} cache[key]['id'] = self[key].id - repositories = [] - for repository in self[key].repositories: - repositories.append(dict(repository._asdict())) - cache[key]['repositories'] = repositories + cache[key]['repositories'] = [repository._asdict() for repository in self[key].repositories] return cache def save(self): self._cache_dict['catalogs'] = self.to_dict() - with open(self._cache_file, 'w+') as file_h: - file_h.write(JSON.dumps(self._cache_dict)) + with open(self._cache_file, 'w') as file_h: + JSON.dump(self._cache_dict, file_h) class ClientCatalogManager(catalog.CatalogManager): """ - Base manager for handling Catalogs + Base manager for handling Catalogs. """ - def __init__(self, manager_type=None, *args, **kwargs): + def __init__(self, manager_type='plugins/client', *args, **kwargs): self._catalog_cache = CatalogCacheManager(os.path.join(application.USER_DATA_PATH, 'cache.json')) super(ClientCatalogManager, self).__init__(*args, **kwargs) - self.manager_type = 'plugins/client' - if manager_type: - self.manager_type = manager_type + self.manager_type = manager_type def get_collection(self, catalog_id, repo_id): """ @@ -650,26 +634,24 @@ def get_collection(self, catalog_id, repo_id): :return: The the collection of manager type from the specified catalog and repository. :rtype:py:class: """ - if self.manager_type not in self.get_repository(catalog_id, repo_id).collections: - return - return self.get_repository(catalog_id, repo_id).collections[self.manager_type] + return self.catalogs[catalog_id].repositories[repo_id].collections.get(self.manager_type) def install_plugin(self, catalog_id, repo_id, plugin_id, install_path): - self.get_repository(catalog_id, repo_id).get_item_files(self.manager_type, plugin_id, install_path) + self.catalogs[catalog_id].repositories[repo_id].get_item_files(self.manager_type, plugin_id, install_path) - def save_catalog_cache(self): + def save_cache(self): for catalog_ in self.catalogs: if catalog_ not in self._catalog_cache: - self._catalog_cache.add_catalog_cache( + self._catalog_cache.cache_catalog_repositories( self.catalogs[catalog_].id, self.get_collections_to_cache(catalog_), ) self._catalog_cache.save() def add_catalog_url(self, url): - super().add_catalog_url(url) + super(ClientCatalogManager, self).add_catalog_url(url) if self.catalogs: - self.save_catalog_cache() + self.save_cache() def is_compatible(self, catalog_id, repo_id, plugin_name): plugin = self.get_collection(catalog_id, repo_id)[plugin_name] @@ -692,10 +674,3 @@ def get_collections_to_cache(self, catalog_): def get_cache(self): return self._catalog_cache - - def get_cache_catalog_ids(self): - for item in self._catalog_cache: - yield item - - def get_cache_repositories(self, catalog_id): - return iter(self._catalog_cache[catalog_id].repositories) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 5f6ce5d3..ee57a8a2 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -30,17 +30,17 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -import sys +import collections import os +import sys import traceback -from collections import namedtuple - from king_phisher import utilities from king_phisher.client import application from king_phisher.client import gui_utilities from king_phisher.client.widget import managers from king_phisher.client.plugins import ClientCatalogManager + from gi.repository import Gdk from gi.repository import Gtk @@ -73,16 +73,27 @@ class PluginManagerWindow(gui_utilities.GladeGObject): ) ) top_gobject = 'window' - + _named_model = collections.namedtuple( + 'model_row', + [ + 'id', + 'installed', + 'enabled', + 'title', + 'compatibility', + 'version', + 'visible_enabled', + 'visible_installed', + 'installed_sensitive', + 'type' + ] + ) def __init__(self, *args, **kwargs): super(PluginManagerWindow, self).__init__(*args, **kwargs) treeview = self.gobjects['treeview_plugins'] self.status_bar = self.gobjects['statusbar'] self._module_errors = {} - tvm = managers.TreeViewManager( - treeview, - cb_refresh=self._load_plugins - ) + tvm = managers.TreeViewManager(treeview, cb_refresh=self._load_plugins) toggle_renderer_enable = Gtk.CellRendererToggle() toggle_renderer_enable.connect('toggled', self.signal_renderer_toggled_enable) toggle_renderer_install = Gtk.CellRendererToggle() @@ -103,28 +114,10 @@ def __init__(self, *args, **kwargs): tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'sensitive', 7) tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 7) tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'sensitive', 8) - self._named_model = namedtuple( - 'model_row', - [ - 'id', - 'installed', - 'enabled', - 'title', - 'compatibility', - 'version', - 'visible_enabled', - 'visible_installed', - 'installed_sensitive', - 'type' - ] - ) self._model = Gtk.TreeStore(str, bool, bool, str, str, str, bool, bool, bool, str) self._model.set_sort_column_id(1, Gtk.SortType.DESCENDING) treeview.set_model(self._model) self.plugin_path = os.path.join(application.USER_DATA_PATH, 'plugins') - if 'catalogs' not in self.config: - self.config['catalogs'] = ['https://raw.githubusercontent.com/securestate/king-phisher-plugins/dev/catalog.json'] - self.catalog_list = self.config['catalogs'] self.load_thread = utilities.Thread(target=self._load_catalogs) self.load_thread.start() self.popup_menu = tvm.get_popup_menu() @@ -136,7 +129,7 @@ def __init__(self, *args, **kwargs): menu_item_reload_all.connect('activate', self.signal_popup_menu_activate_reload_all) self.popup_menu.append(menu_item_reload_all) self.popup_menu.show_all() - self._update_status_bar('Loading: ') + self._update_status_bar('Loading...') self.window.show() selection = treeview.get_selection() @@ -155,8 +148,8 @@ def signal_window_show(self, _): def _load_catalogs(self): self._update_status_bar('Loading: Downloading Catalogs', idle=True) self.catalog_plugins = ClientCatalogManager() - for catalog in self.catalog_list: - self.logger.debug("downloading catalog {}".format(catalog)) + for catalog in self.config['plugins.catalogs']: + self.logger.debug("downloading catalog: {}".format(catalog)) self._update_status_bar("Loading: Downloading Catalog: {}".format(catalog)) self.catalog_plugins.add_catalog_url(catalog) self._load_plugins() @@ -171,10 +164,6 @@ def _update_status_bar(self, string_to_set, idle=False): else: self.__update_status_bar(string_to_set) - def _model_item(self, model_path, item): - named_row = self._named_model(*self._model[model_path]) - return getattr(named_row, item) - def _set_model_item(self, model_path, item, item_value): self._model[model_path][self._named_model._fields.index(item)] = item_value @@ -188,7 +177,11 @@ def _toggle_cell_data_func(self, column, cell, model, tree_iter, _): cell.set_property('inconsistent', False) def _store_append(self, store, parent, model): - return store.append(parent, model) + return store.append(parent, model) + + def _store_extend(self, store, parent, models): + for model in models: + store.append(parent, model) def _load_plugins(self): """ @@ -196,7 +189,7 @@ def _load_plugins(self): visible to the user. """ self.logger.debug('loading plugins') - self._update_status_bar('Loading.... Plugins....', idle=True) + self._update_status_bar('Loading... Plugins...', idle=True) store = self._model store.clear() pm = self.application.plugin_manager @@ -204,22 +197,24 @@ def _load_plugins(self): pm.load_all(on_error=self._on_plugin_load_error) model = ('local', None, True, '[Locally Installed]', None, None, False, False, False, 'Catalog') catalog = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) + models = [] for name, plugin in pm.loaded_plugins.items(): if name in self.config['plugins.installed']: continue - model = ( - plugin.name, - True, - plugin.name in pm.enabled_plugins, - plugin.title, - 'Yes' if plugin.is_compatible else 'No', - plugin.version, - True, - True, - False, - 'Plugin' - ) - gui_utilities.glib_idle_add_once(self._store_append, store, catalog, model) + models.append(self._named_model( + id=plugin.name, + installed=True, + enabled=plugin.name in pm.enabled_plugins, + title=plugin.title, + compatibility='Yes' if plugin.is_compatible else 'No', + version=plugin.version, + visible_enabled=True, + visible_installed=True, + installed_sensitive=False, + type='Plugin' + )) + gui_utilities.glib_idle_add_once(self._store_append, store, catalog, models[-1]) + del models for name in self._module_errors.keys(): model = (name, True, False, "{0} (Load Failed)".format(name), 'unknown', True, True, False, 'Plugin') @@ -238,10 +233,10 @@ def _load_plugins(self): else: if not self.config['plugins.installed']: return - for catalog_id in self.catalog_plugins.get_cache_catalog_ids(): + for catalog_id, repositories in self.catalog_plugins.get_cache().items(): model = (catalog_id, None, True, "{} (offline)".format(catalog_id), None, None, False, False, False, 'Catalog (offline)') catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) - for repo in self.catalog_plugins.get_cache_repositories(catalog_id): + for repo in repositories: model = (repo.id, None, True, "{} (offline)".format(repo.title), None, None, False, False, False, 'Repository (offline)') repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) self._add_plugins_offline(catalog_id, repo.id, store, repo_line) @@ -250,46 +245,48 @@ def _load_plugins(self): def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): client_plugins = list(plugin_list) - for plugins in client_plugins: + models = [] + for plugin in client_plugins: installed = False enabled = False - if plugin_list[plugins]['name'] in self.config['plugins.installed']: - if repo.id == self.config['plugins.installed'][plugin_list[plugins]['name']][1]: + if plugin_list[plugin]['name'] in self.config['plugins.installed']: + if repo.id == self.config['plugins.installed'][plugin_list[plugin]['name']][1]: installed = True - enabled = True if plugin_list[plugins]['name'] in self.config['plugins.enabled'] else False - model = ( - plugins, - installed, - enabled, - plugin_list[plugins]['title'], - 'Yes' if self.catalog_plugins.is_compatible(catalog, repo.id, plugins) else 'No', - self._get_version_or_upgrade(plugin_list[plugins]['name'], plugin_list[plugins]['version']), - True, - True, - True, - 'Plugin' - ) - gui_utilities.glib_idle_add_once(self._store_append, store, parent, model) + enabled = plugin_list[plugin]['name'] in self.config['plugins.enabled'] + models.append(self._named_model( + id=plugin, + installed=installed, + enabled=enabled, + title=plugin_list[plugin]['title'], + compatibility='Yes' if self.catalog_plugins.is_compatible(catalog, repo.id, plugin) else 'No', + version=self._get_version_or_upgrade(plugin_list[plugin]['name'], plugin_list[plugin]['version']), + visible_enabled=True, + visible_installed=True, + installed_sensitive=True, + type='Plugin' + )) + gui_utilities.glib_idle_add_once(self._store_extend, store, parent, models) def _add_plugins_offline(self, catalog_id, repo_id, store, parent): + models = [] for plugin in self.config['plugins.installed']: if self.config['plugins.installed'][plugin][0] != catalog_id: continue if self.config['plugins.installed'][plugin][1] != repo_id: continue - model = ( - plugin, - True, - True if plugin in self.config['plugins.enabled'] else False, - self.application.plugin_manager[plugin].title, - 'Yes' if self.application.plugin_manager[plugin].is_compatible else 'No', - self.application.plugin_manager[plugin].version, - True, - True, - False, - 'Plugin' - ) - gui_utilities.glib_idle_add_once(self._store_append, store, parent, model) + models.append(self._named_model( + id=plugin, + installed=True, + enabled=plugin in self.config['plugins.enabled'], + title=self.application.plugin_manager[plugin].title, + compatibility='Yes' if self.application.plugin_manager[plugin].is_compatible else 'No', + version=self.application.plugin_manager[plugin].version, + visible_enabled=True, + visible_installed=True, + installed_sensitive=False, + type='Plugin' + )) + gui_utilities.glib_idle_add_once(self._store_extend, store, parent, models) def _get_version_or_upgrade(self, plugin_name, plugin_version): if plugin_name not in self.application.plugin_manager: @@ -304,7 +301,7 @@ def signal_popup_menu_activate_reload_all(self, _): def signal_destory(self, _): if self.catalog_plugins: - self.catalog_plugins.save_catalog_cache() + self.catalog_plugins.save_cache() def signal_treeview_row_activated(self, treeview, path, column): self._set_plugin_info(self._model[path]) @@ -373,57 +370,59 @@ def signal_popup_menu_activate_reload(self, _): selected_plugin = model[tree_paths[0]][0] for tree_iter in gui_utilities.gtk_treeview_selection_iterate(treeview): - if self._model_item(tree_iter, 'type') != 'Plugin': + named_row = self._named_model(*self._model[tree_iter]) + if named_row.type != 'Plugin': continue - plugin_id = self._model_item(tree_iter, 'id') - enabled = plugin_id in pm.enabled_plugins - pm.unload(plugin_id) + enabled = named_row.id in pm.enabled_plugins + pm.unload(named_row.id) try: - klass = pm.load(plugin_id, reload_module=True) + klass = pm.load(named_row.id, reload_module=True) except Exception as error: - self._on_plugin_load_error(plugin_id, error) - if plugin_id == selected_plugin: - self._set_plugin_info(plugin_id) - self._set_model_item(tree_iter, 'title', "{0} (Reload Failed)".format(plugin_id)) + self._on_plugin_load_error(named_row.id, error) + if named_row.id == selected_plugin: + self._set_plugin_info(named_row.id) + self._set_model_item(tree_iter, 'title', "{0} (Reload Failed)".format(named_row.id)) continue - if plugin_id in self._module_errors: - del self._module_errors[plugin_id] + if named_row.id in self._module_errors: + del self._module_errors[named_row.id] self._set_model_item(tree_iter, 'title', klass.title) - if plugin_id == selected_plugin: - self._set_plugin_info(plugin_id) + if named_row.id == selected_plugin: + self._set_plugin_info(named_row.id) if enabled: - pm.enable(plugin_id) + pm.enable(named_row.id) def signal_renderer_toggled_enable(self, _, path): pm = self.application.plugin_manager - if self._model_item(path, 'type') != 'Plugin': + named_row = self._named_model(*self._model[path]) + if named_row.type != 'Plugin': return - if self._model_item(path, 'id') not in pm.loaded_plugins: + if named_row.id not in pm.loaded_plugins: return if self._model[path].parent: - installed_plugin_info = self.config['plugins.installed'][self._model_item(path, 'id')] + installed_plugin_info = self.config['plugins.installed'][named_row.id] repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) if repo_model.id != installed_plugin_info[1] or catalog_model.id != installed_plugin_info[0]: return - if self._model_item(path, 'id') in self._module_errors: + if named_row.id in self._module_errors: gui_utilities.show_dialog_error('Can Not Enable Plugin', self.window, 'Can not enable a plugin which failed to load.') return - if self._model_item(path, 'enabled'): + if named_row.enabled: self._disable_plugin(path) else: - if not pm.loaded_plugins[self._model_item(path, 'id')].is_compatible: + if not pm.loaded_plugins[named_row.id].is_compatible: gui_utilities.show_dialog_error('Incompatible Plugin', self.window, 'This plugin is not compatible.') return - if not pm.enable(self._model_item(path, 'id')): + if not pm.enable(named_row.id): return self._set_model_item(path, 'enabled', True) - self.config['plugins.enabled'].append(self._model_item(path, 'id')) + self.config['plugins.enabled'].append(named_row.id) def signal_renderer_toggled_install(self, _, path): repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) plugin_collection = self.catalog_plugins.get_collection(catalog_model.id, repo_model.id) - if self._model_item(path, 'installed'): - if self._model_item(path, 'enabled'): + named_row = self._named_model(*self._model[path]) + if named_row.installed: + if named_row.enabled: response = gui_utilities.show_dialog_yes_no( 'Plugin is enabled', self.window, @@ -433,10 +432,10 @@ def signal_renderer_toggled_install(self, _, path): return self._disable_plugin(path) self._uninstall_plugin(plugin_collection, path) - self.logger.info("uninstalled plugin {}".format(self._model_item(path, 'id'))) + self.logger.info("uninstalled plugin {}".format(named_row.id)) else: - if self._model_item(path, 'id') in self.config['plugins.installed']: - installed_plugin_info = self.config['plugins.installed'][self._model_item(path, 'id')] + if named_row.id in self.config['plugins.installed']: + installed_plugin_info = self.config['plugins.installed'][named_row.id] if installed_plugin_info != [catalog_model.id, repo_model.id]: window_question = "A plugin with this name is already installed from Catalog: {} Repo: {}\nDo you want to replace it with this one?" response = gui_utilities.show_dialog_yes_no( @@ -447,38 +446,26 @@ def signal_renderer_toggled_install(self, _, path): if not response: return if not self._remove_matching_plugin(path, installed_plugin_info): - self.logger.warning('failed to uninstall plugin {}'.format(self._model_item(path, 'id'))) + self.logger.warning('failed to uninstall plugin {}'.format(named_row.id)) return - self.catalog_plugins.install_plugin( - catalog_model.id, - repo_model.id, - self._model_item(path, 'id'), - self.plugin_path - ) - self.config['plugins.installed'][self._model_item(path, 'id')] = [catalog_model.id, repo_model.id] + self.catalog_plugins.install_plugin(catalog_model.id, repo_model.id, named_row.id, self.plugin_path) + self.config['plugins.installed'][named_row.id] = [catalog_model.id, repo_model.id] self._set_model_item(path, 'installed', True) - self._set_model_item( - path, - 'version', - self.catalog_plugins.get_collection(catalog_model.id, repo_model.id)[self._model_item(path, 'id')]['version'] - ) - self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(self._model_item(path, 'id'), catalog_model.id, repo_model.id)) + self._set_model_item(path, 'version', self.catalog_plugins.get_collection(catalog_model.id, repo_model.id)[named_row.id]['version']) + self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(named_row.id, catalog_model.id, repo_model.id)) self.application.plugin_manager.load_all(on_error=self._on_plugin_load_error) - def _disable_plugin(self, path, model_path=True): - if not model_path: - named_plugin = self._named_model(*path) - self.application.plugin_manager.disable(named_plugin.id) - self.config['plugins.enabled'].remove(named_plugin.id) - path[self._named_model._fields.index('enabled')] = False - else: - plugin_id = self._model_item(path, 'id') - self.application.plugin_manager.disable(plugin_id) - self.config['plugins.enabled'].remove(plugin_id) + def _disable_plugin(self, path, is_path=True): + named_row = self._named_model(*(self._model[path] if is_path else path)) + self.application.plugin_manager.disable(named_row.id) + self.config['plugins.enabled'].remove(named_row.id) + if is_path: self._set_model_item(path, 'enabled', False) + else: + path[self._named_model._fields.index('enabled')] = False def _remove_matching_plugin(self, path, installed_plugin_info): - plugin_id = self._model_item(path, 'id') + named_row = self._named_model(*self._model[path]) for catalog_model in self._model: if self._named_model(*catalog_model).id != installed_plugin_info[0]: continue @@ -487,15 +474,12 @@ def _remove_matching_plugin(self, path, installed_plugin_info): continue for plugin_model in repo_model.iterchildren(): named_model = self._named_model(*plugin_model) - if named_model.id != plugin_id: + if named_model.id != named_row.id: continue if named_model.enabled: - self._disable_plugin(plugin_model, model_path=False) + self._disable_plugin(plugin_model, is_path=False) self._uninstall_plugin( - self.catalog_plugins.get_collection( - installed_plugin_info[0], - installed_plugin_info[1] - ), + self.catalog_plugins.get_collection(installed_plugin_info[0], installed_plugin_info[1]), plugin_model, is_path=False ) @@ -505,10 +489,7 @@ def _get_plugin_model_parents(self, plugin_model): return self._named_model(*plugin_model.parent), self._named_model(*plugin_model.parent.parent) def _uninstall_plugin(self, plugin_collection, path, is_path=True): - if is_path: - plugin_id = self._model_item(path, 'id') - else: - plugin_id = self._named_model(*path).id + plugin_id = self._named_model(*(self._model[path] if is_path else path)).id for files in plugin_collection[plugin_id]['files']: file_name = files[0] if os.path.isfile(os.path.join(self.plugin_path, file_name)): @@ -545,68 +526,51 @@ def _set_non_plugin_info(self, model_instance): return textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() - text = '' if named_model.type == 'Catalog': instance_information = self.catalog_plugins.catalogs[named_model.id] else: - instance_information = self.catalog_plugins.get_repository(self._named_model(*model_instance.parent).id, named_model.id) + instance_information = self.catalog_plugins.catalogs[self._named_model(*model_instance.parent).id].repositories[named_model.id] - text += "{0}\n".format(named_model.type) + text = "{0}\n".format(named_model.type) text += "Title: {0}\n".format(getattr(instance_information, 'title', 'None')) text += "Id: {0}\n".format(getattr(instance_information, 'id', 'None')) text += "Description: {}\n".format(getattr(instance_information, 'description', 'None')) - if hasattr(instance_information, 'maintainers'): - text += 'Maintainer: ' + '\nMaintainer: '.join(getattr(instance_information, 'maintainers', ['None'])) + '\n' - - buf.insert(buf.get_end_iter(), "{0}\n".format(text), -1) + if getattr(instance_information, 'maintainers', None): + text += 'Maintainer: ' + '\nMaintainer: '.join(instance_information.maintainers) + '\n' + buf.insert(buf.get_end_iter(), text, -1) def _set_non_plugin_offline_info(self, model_instance): named_model = self._named_model(*model_instance) textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() - text = '{0}\n'.format(named_model.type.split()[0]) - text += "Title: {0}".format(named_model.title) - buf.insert(buf.get_end_iter(), "{0}\n".format(text), -1) + text = named_model.type.split()[0] + '\n' + text += "Title: {0}\n".format(named_model.title) + buf.insert(buf.get_end_iter(), text, -1) def _set_plugin_info_details(self, plugin_model): named_model = self._named_model(*plugin_model) - model_id = named_model.id pm = self.application.plugin_manager self._last_plugin_selected = plugin_model - if model_id in pm.loaded_plugins: - klass = pm.loaded_plugins[model_id] - self.gobjects['label_plugin_info_title'].set_text(klass.title) - self.gobjects['label_plugin_info_compatible'].set_text('Yes' if klass.is_compatible else 'No') - self.gobjects['label_plugin_info_version'].set_text(klass.version) - self.gobjects['label_plugin_info_authors'].set_text('\n'.join(klass.authors)) - label_homepage = self.gobjects['label_plugin_info_homepage'] - if klass.homepage is None: - label_homepage.set_property('visible', False) - else: - label_homepage.set_markup("Homepage".format(klass.homepage)) - label_homepage.set_property('tooltip-text', klass.homepage) - label_homepage.set_property('visible', True) - self.gobjects['label_plugin_info_description'].set_text(klass.description) + if named_model.id in pm.loaded_plugins: + plugin = pm.loaded_plugins[named_model.id].metadata + is_compatible = plugin['is_compatible'] else: repo_model, catalog_model = self._get_plugin_model_parents(plugin_model) - for repo in self.catalog_plugins.get_repositories(catalog_model.id): - if repo.id != repo_model.id: - continue - plugin = repo.collections['plugins/client'][named_model.id] - self.gobjects['label_plugin_info_title'].set_text(plugin['title']) - self.gobjects['label_plugin_info_compatible'].set_text( - 'Yes' if self.catalog_plugins.is_compatible(catalog_model.id, repo_model.id, named_model.id) else 'No' - ) - self.gobjects['label_plugin_info_version'].set_text(plugin['version']) - self.gobjects['label_plugin_info_authors'].set_text('\n'.join(plugin['authors'])) - label_homepage = self.gobjects['label_plugin_info_homepage'] - if plugin['homepage'] is None: - label_homepage.set_property('visible', False) - else: - label_homepage.set_markup("Homepage".format(plugin['homepage'])) - label_homepage.set_property('tooltip-text', plugin['homepage']) - label_homepage.set_property('visible', True) - self.gobjects['label_plugin_info_description'].set_text(plugin['description']) + plugin = self.catalog_plugins.get_collection(catalog_model.id, repo_model.id)[named_model.id] + is_compatible = self.catalog_plugins.is_compatible(catalog_model.id, repo_model.id, named_model.id) + + self.gobjects['label_plugin_info_title'].set_text(plugin['title']) + self.gobjects['label_plugin_info_compatible'].set_text('Yes' if is_compatible else 'No') + self.gobjects['label_plugin_info_version'].set_text(plugin['version']) + self.gobjects['label_plugin_info_authors'].set_text('\n'.join(plugin['authors'])) + label_homepage = self.gobjects['label_plugin_info_homepage'] + if plugin['homepage'] is None: + label_homepage.set_property('visible', False) + else: + label_homepage.set_markup("Homepage".format(plugin['homepage'])) + label_homepage.set_property('tooltip-text', plugin['homepage']) + label_homepage.set_property('visible', True) + self.gobjects['label_plugin_info_description'].set_text(plugin['description']) def _set_plugin_info_error(self, model_instance): id_ = self._named_model(*model_instance).id diff --git a/king_phisher/plugins.py b/king_phisher/plugins.py index 48cf3d07..9ffac38c 100644 --- a/king_phisher/plugins.py +++ b/king_phisher/plugins.py @@ -172,7 +172,6 @@ def _check_for_missing_packages(self, packages): if package.startswith('gi.'): if not importlib.util.find_spec(package): missing_packages.append(package) - logging.info('During requirement check, gi subpackage was manually checked') continue package_check = smoke_zephyr.requirements.check_requirements([package]) if package_check: @@ -251,6 +250,7 @@ def metadata(cls): 'description': cls.description, 'homepage': cls.homepage, 'name': cls.name, + 'is_compatible': cls.is_compatible, 'requirements': { 'minimum-version': cls.req_min_version, 'packages': tuple(cls.req_packages.keys()) diff --git a/king_phisher/utilities.py b/king_phisher/utilities.py index 6b2d1de9..3093d943 100644 --- a/king_phisher/utilities.py +++ b/king_phisher/utilities.py @@ -488,10 +488,9 @@ class Thread(threading.Thread): """ King Phishers base threading class with two way event. """ - + logger = logging.getLogger('KingPhisher.Thread') def __init__(self, *args, **kwargs): super(Thread, self).__init__(*args, **kwargs) - self.logger = logging.getLogger('KingPhisher.Thread.{0}'.format(self.name)) self.stop_flag = Event() self.stop_flag.clear() @@ -501,7 +500,7 @@ def run(self): def stop(self): """ - Sets the flag to signal the threat to stop. + Sets the flag to signal the thread to stop. """ self.stop_flag.set() From 326d035c141552ba6edc1b30f13cb42bff3d52b9 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 31 Oct 2017 09:12:14 -0400 Subject: [PATCH 15/38] Additional plugin manager refactoring --- data/client/king_phisher/client_config.json | 6 +- king_phisher/client/windows/plugin_manager.py | 110 ++++++++---------- king_phisher/utilities.py | 9 +- 3 files changed, 60 insertions(+), 65 deletions(-) diff --git a/data/client/king_phisher/client_config.json b/data/client/king_phisher/client_config.json index 25c10dc8..324841b1 100644 --- a/data/client/king_phisher/client_config.json +++ b/data/client/king_phisher/client_config.json @@ -1,4 +1,7 @@ { + "catalogs": [ + "https://raw.githubusercontent.com/securestate/king-phisher-plugins/master/catalog.json" + ], "dashboard.bottom": "VisitsTimeline", "dashboard.top_left": "Overview", "dashboard.top_right": "VisitorInfo", @@ -12,9 +15,6 @@ "filter.campaign.user": true, "filter.campaign.other_users": false, "plugins": {}, - "plugins.catalogs": [ - "https://raw.githubusercontent.com/securestate/king-phisher-plugins/master/catalog.json" - ], "plugins.enabled": [], "plugins.installed": {}, "rpc.serializer": null, diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index ee57a8a2..0626949d 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -46,6 +46,10 @@ __all__ = ('PluginManagerWindow',) +_ROW_TYPE_PLUGIN = 'plugin' +_ROW_TYPE_REPOSITORY = 'repository' +_ROW_TYPE_CATALOG = 'catalog' + class PluginManagerWindow(gui_utilities.GladeGObject): """ The window which allows the user to selectively enable and disable plugins @@ -84,7 +88,7 @@ class PluginManagerWindow(gui_utilities.GladeGObject): 'version', 'visible_enabled', 'visible_installed', - 'installed_sensitive', + 'sensitive_installed', 'type' ] ) @@ -148,7 +152,7 @@ def signal_window_show(self, _): def _load_catalogs(self): self._update_status_bar('Loading: Downloading Catalogs', idle=True) self.catalog_plugins = ClientCatalogManager() - for catalog in self.config['plugins.catalogs']: + for catalog in self.config['catalogs']: self.logger.debug("downloading catalog: {}".format(catalog)) self._update_status_bar("Loading: Downloading Catalog: {}".format(catalog)) self.catalog_plugins.add_catalog_url(catalog) @@ -195,7 +199,7 @@ def _load_plugins(self): pm = self.application.plugin_manager self._module_errors = {} pm.load_all(on_error=self._on_plugin_load_error) - model = ('local', None, True, '[Locally Installed]', None, None, False, False, False, 'Catalog') + model = (None, None, True, '[Locally Installed]', None, None, False, False, False, _ROW_TYPE_CATALOG) catalog = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) models = [] for name, plugin in pm.loaded_plugins.items(): @@ -210,34 +214,34 @@ def _load_plugins(self): version=plugin.version, visible_enabled=True, visible_installed=True, - installed_sensitive=False, - type='Plugin' + sensitive_installed=False, + type=_ROW_TYPE_PLUGIN )) gui_utilities.glib_idle_add_once(self._store_append, store, catalog, models[-1]) del models for name in self._module_errors.keys(): - model = (name, True, False, "{0} (Load Failed)".format(name), 'unknown', True, True, False, 'Plugin') + model = (name, True, False, "{0} (Load Failed)".format(name), 'unknown', True, True, False, _ROW_TYPE_PLUGIN) gui_utilities.glib_idle_add_once(self._store_append, store, catalog, model) self.logger.debug('loading catalog into plugin treeview') if self.catalog_plugins.catalog_ids(): - for catalogs in self.catalog_plugins.catalog_ids(): - model = (catalogs, None, True, catalogs, None, None, False, False, False, 'Catalog') + for catalog_id in self.catalog_plugins.catalog_ids(): + model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) catalog = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) - for repos in self.catalog_plugins.get_repositories(catalogs): - model = (repos.id, None, True, repos.title, None, None, False, False, False, 'Repository') + for repos in self.catalog_plugins.get_repositories(catalog_id): + model = (repos.id, None, True, repos.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog, model) - plugin_collections = self.catalog_plugins.get_collection(catalogs, repos.id) - self._add_plugins_to_tree(catalogs, repos, store, repo_line, plugin_collections) + plugin_collections = self.catalog_plugins.get_collection(catalog_id, repos.id) + self._add_plugins_to_tree(catalog_id, repos, store, repo_line, plugin_collections) else: if not self.config['plugins.installed']: return for catalog_id, repositories in self.catalog_plugins.get_cache().items(): - model = (catalog_id, None, True, "{} (offline)".format(catalog_id), None, None, False, False, False, 'Catalog (offline)') + model = (catalog_id, None, True, "{} (offline)".format(catalog_id), None, None, False, False, False, _ROW_TYPE_CATALOG) catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) for repo in repositories: - model = (repo.id, None, True, "{} (offline)".format(repo.title), None, None, False, False, False, 'Repository (offline)') + model = (repo.id, None, True, "{} (offline)".format(repo.title), None, None, False, False, False, _ROW_TYPE_REPOSITORY) repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) self._add_plugins_offline(catalog_id, repo.id, store, repo_line) @@ -262,8 +266,8 @@ def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): version=self._get_version_or_upgrade(plugin_list[plugin]['name'], plugin_list[plugin]['version']), visible_enabled=True, visible_installed=True, - installed_sensitive=True, - type='Plugin' + sensitive_installed=True, + type=_ROW_TYPE_PLUGIN )) gui_utilities.glib_idle_add_once(self._store_extend, store, parent, models) @@ -283,8 +287,8 @@ def _add_plugins_offline(self, catalog_id, repo_id, store, parent): version=self.application.plugin_manager[plugin].version, visible_enabled=True, visible_installed=True, - installed_sensitive=False, - type='Plugin' + sensitive_installed=False, + type=_ROW_TYPE_PLUGIN )) gui_utilities.glib_idle_add_once(self._store_extend, store, parent, models) @@ -371,7 +375,7 @@ def signal_popup_menu_activate_reload(self, _): for tree_iter in gui_utilities.gtk_treeview_selection_iterate(treeview): named_row = self._named_model(*self._model[tree_iter]) - if named_row.type != 'Plugin': + if named_row.type != _ROW_TYPE_PLUGIN: continue enabled = named_row.id in pm.enabled_plugins pm.unload(named_row.id) @@ -394,7 +398,7 @@ def signal_popup_menu_activate_reload(self, _): def signal_renderer_toggled_enable(self, _, path): pm = self.application.plugin_manager named_row = self._named_model(*self._model[path]) - if named_row.type != 'Plugin': + if named_row.type != _ROW_TYPE_PLUGIN: return if named_row.id not in pm.loaded_plugins: return @@ -508,44 +512,29 @@ def _set_plugin_info(self, model_instance): buf = textview.get_buffer() buf.delete(buf.get_start_iter(), buf.get_end_iter()) model_id = named_model.id - if named_model.type != 'Plugin': - stack.set_visible_child(textview) - self._set_non_plugin_info(model_instance) - return - if model_id in self._module_errors: - stack.set_visible_child(textview) - self._set_plugin_info_error(model_id) + if named_model.type == _ROW_TYPE_PLUGIN: + if model_id in self._module_errors: + stack.set_visible_child(textview) + self._set_plugin_info_error(model_id) + else: + stack.set_visible_child(self.gobjects['grid_plugin_info']) + self._set_plugin_info_details(model_instance) else: - stack.set_visible_child(self.gobjects['grid_plugin_info']) - self._set_plugin_info_details(model_instance) + self._set_non_plugin_info(model_instance) def _set_non_plugin_info(self, model_instance): named_model = self._named_model(*model_instance) - if 'offline' in named_model.type or named_model.id == 'local': - self._set_non_plugin_offline_info(model_instance) + self.gobjects['label_plugin_info_homepage'].set_property('visible', False) + self.gobjects['label_plugin_info_title'].set_text(named_model.title) + if named_model.title.lower().endswith(' (offline)') or (named_model.id is None and named_model.type == _ROW_TYPE_CATALOG): return - textview = self.gobjects['textview_plugin_info'] - buf = textview.get_buffer() - if named_model.type == 'Catalog': - instance_information = self.catalog_plugins.catalogs[named_model.id] + if named_model.type == _ROW_TYPE_CATALOG: + obj = self.catalog_plugins.catalogs[named_model.id] else: - instance_information = self.catalog_plugins.catalogs[self._named_model(*model_instance.parent).id].repositories[named_model.id] - - text = "{0}\n".format(named_model.type) - text += "Title: {0}\n".format(getattr(instance_information, 'title', 'None')) - text += "Id: {0}\n".format(getattr(instance_information, 'id', 'None')) - text += "Description: {}\n".format(getattr(instance_information, 'description', 'None')) - if getattr(instance_information, 'maintainers', None): - text += 'Maintainer: ' + '\nMaintainer: '.join(instance_information.maintainers) + '\n' - buf.insert(buf.get_end_iter(), text, -1) - - def _set_non_plugin_offline_info(self, model_instance): - named_model = self._named_model(*model_instance) - textview = self.gobjects['textview_plugin_info'] - buf = textview.get_buffer() - text = named_model.type.split()[0] + '\n' - text += "Title: {0}\n".format(named_model.title) - buf.insert(buf.get_end_iter(), text, -1) + obj = self.catalog_plugins.catalogs[self._named_model(*model_instance.parent).id].repositories[named_model.id] + if getattr(obj, 'description', None): + self.gobjects['label_plugin_info_description'].set_text(obj.description) + self._set_homepage_url(getattr(obj, 'homepage', None)) def _set_plugin_info_details(self, plugin_model): named_model = self._named_model(*plugin_model) @@ -563,14 +552,8 @@ def _set_plugin_info_details(self, plugin_model): self.gobjects['label_plugin_info_compatible'].set_text('Yes' if is_compatible else 'No') self.gobjects['label_plugin_info_version'].set_text(plugin['version']) self.gobjects['label_plugin_info_authors'].set_text('\n'.join(plugin['authors'])) - label_homepage = self.gobjects['label_plugin_info_homepage'] - if plugin['homepage'] is None: - label_homepage.set_property('visible', False) - else: - label_homepage.set_markup("Homepage".format(plugin['homepage'])) - label_homepage.set_property('tooltip-text', plugin['homepage']) - label_homepage.set_property('visible', True) self.gobjects['label_plugin_info_description'].set_text(plugin['description']) + self._set_homepage_url(plugin['homepage']) def _set_plugin_info_error(self, model_instance): id_ = self._named_model(*model_instance).id @@ -579,3 +562,12 @@ def _set_plugin_info_error(self, model_instance): exc, formatted_exc = self._module_errors[id_] buf.insert(buf.get_end_iter(), "{0!r}\n\n".format(exc), -1) buf.insert(buf.get_end_iter(), ''.join(formatted_exc), -1) + + def _set_homepage_url(self, url=None): + label_homepage = self.gobjects['label_plugin_info_homepage'] + if url is None: + label_homepage.set_property('visible', False) + return + label_homepage.set_markup("Homepage".format(url)) + label_homepage.set_property('tooltip-text', url) + label_homepage.set_property('visible', True) diff --git a/king_phisher/utilities.py b/king_phisher/utilities.py index 3093d943..fa014843 100644 --- a/king_phisher/utilities.py +++ b/king_phisher/utilities.py @@ -489,13 +489,16 @@ class Thread(threading.Thread): King Phishers base threading class with two way event. """ logger = logging.getLogger('KingPhisher.Thread') - def __init__(self, *args, **kwargs): - super(Thread, self).__init__(*args, **kwargs) + def __init__(self, target=None, name=None, args=(), kwargs={}, **_kwargs): + super(Thread, self).__init__(target=target, name=name, args=args, kwargs=kwargs, **_kwargs) + self.target_name = None + if target is not None: + self.target_name = target.__module__ + '.' + target.__name__ self.stop_flag = Event() self.stop_flag.clear() def run(self): - self.logger.debug("thread {0} running in tid: 0x{1:x}".format(self.name, threading.current_thread().ident)) + self.logger.debug("thread {0} running {1} in tid: 0x{2:x}".format(self.name, self.target_name, threading.current_thread().ident)) super(Thread, self).run() def stop(self): From d30dd84ab60386faed3c89bce628c7264b14da9c Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Wed, 1 Nov 2017 17:29:49 -0400 Subject: [PATCH 16/38] Added stack for Catalog and Repo pluginwindow information --- .../king_phisher/king-phisher-client.ui | 124 +++++++++++++++++- king_phisher/catalog.py | 14 +- king_phisher/client/plugins.py | 77 +++++++++-- king_phisher/client/windows/plugin_manager.py | 116 ++++++++++------ 4 files changed, 274 insertions(+), 57 deletions(-) diff --git a/data/client/king_phisher/king-phisher-client.ui b/data/client/king_phisher/king-phisher-client.ui index f802077c..b41a5881 100644 --- a/data/client/king_phisher/king-phisher-client.ui +++ b/data/client/king_phisher/king-phisher-client.ui @@ -2761,12 +2761,12 @@ Spencer McIntyre - + True True - + 125 True True @@ -2774,11 +2774,11 @@ Spencer McIntyre 3 in - + True False - + True False @@ -2976,6 +2976,122 @@ Spencer McIntyre 1 + + + True + False + 3 + 3 + True + + + True + False + False + + + + + + + 0 + 0 + 4 + + + + + True + False + False + end + start + Maintainers: + + + + + + 0 + 1 + + + + + True + False + False + The contributors who have written and provided this plugin. + start + start + + + + + + 1 + 1 + + + + + True + False + False + end + start + Description: + + + + + + 0 + 2 + + + + + True + False + The description of this plugin and the features it provides. + start + start + True + True + + + + + + 1 + 2 + 3 + + + + + True + True + <a href="">Homepage</a> + True + False + + + 2 + 1 + + + + + + + + page2 + page2 + 2 + + diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index 1268f071..9172e196 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -377,11 +377,19 @@ def get_repositories(self, catalog_id): return tuple(self.catalogs[catalog_id].repositories.values()) def add_catalog_url(self, url): + """ + Adds catalog to the manager by its url. + :param url: url of the catalog which to load + :return: The catalog + :rtype: :py:class: `.Catalog` + """ try: - c = Catalog.from_url(url) - self.catalogs[c.id] = c + catalog = Catalog.from_url(url) + self.catalogs[catalog.id] = catalog except Exception as error: - self.logger.warning("failed to load catalog from url {0} due to {1}".format(url, error), exc_info=True) + self.logger.warning("failed to load catalog from url {0} due to {1}".format(url, error)) + return + return catalog def sign_item_files(local_path, signing_key, signing_key_id, repo_path=None): """ diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 0ee0701b..0c4cde20 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -31,6 +31,7 @@ # import collections +import datetime import os import sys import tempfile @@ -38,7 +39,6 @@ from king_phisher import catalog from king_phisher import plugins -from king_phisher.client import application from king_phisher.client import gui_utilities from king_phisher.client import mailer from king_phisher.client.widget import extras @@ -573,7 +573,7 @@ def __init__(self, cache_file): self._cache_dict = {} if self._cache_dict and 'catalogs' in self._cache_dict: - cache_cat = self._cache_dict['catalogs'] + cache_cat = self._cache_dict['catalogs']['values'] for catalog_ in cache_cat: self[catalog_] = self._CatalogCacheEntry( cache_cat[catalog_]['id'], @@ -612,7 +612,8 @@ def to_dict(self): return cache def save(self): - self._cache_dict['catalogs'] = self.to_dict() + self._cache_dict['catalogs']['timestamp'] = datetime.datetime.utcnow() + self._cache_dict['catalogs']['values'] = self.to_dict() with open(self._cache_file, 'w') as file_h: JSON.dump(self._cache_dict, file_h) @@ -620,8 +621,8 @@ class ClientCatalogManager(catalog.CatalogManager): """ Base manager for handling Catalogs. """ - def __init__(self, manager_type='plugins/client', *args, **kwargs): - self._catalog_cache = CatalogCacheManager(os.path.join(application.USER_DATA_PATH, 'cache.json')) + def __init__(self, user_data_path, manager_type='plugins/client', *args, **kwargs): + self._catalog_cache = CatalogCacheManager(os.path.join(user_data_path, 'cache.json')) super(ClientCatalogManager, self).__init__(*args, **kwargs) self.manager_type = manager_type @@ -637,31 +638,76 @@ def get_collection(self, catalog_id, repo_id): return self.catalogs[catalog_id].repositories[repo_id].collections.get(self.manager_type) def install_plugin(self, catalog_id, repo_id, plugin_id, install_path): + """ + Installs the specified plugin to the desired plugin path. + :param catalog_id: The id of the catalog of the desired plugin to install. + :param repo_id: The id of the repository of the desired plugin to install. + :param plugin_id: The id of the plugin to install. + :param install_path: The path to install the plugin too. + """ self.catalogs[catalog_id].repositories[repo_id].get_item_files(self.manager_type, plugin_id, install_path) - def save_cache(self): - for catalog_ in self.catalogs: - if catalog_ not in self._catalog_cache: + def save_cache(self, catalog=None): + """ + saves the catalog or catalogs in the manager to the cache + :param catalog: the :py:class: `~king_phisher.Catalog` to save + """ + if catalog: + self._catalog_cache.cache_catalog_repositories( + catalog.id, + self.get_collections_to_cache(catalog.id) + ) + else: + for catalog__ in self.catalogs: self._catalog_cache.cache_catalog_repositories( - self.catalogs[catalog_].id, - self.get_collections_to_cache(catalog_), + self.catalogs[catalog__].id, + self.get_collections_to_cache(catalog__), ) self._catalog_cache.save() def add_catalog_url(self, url): - super(ClientCatalogManager, self).add_catalog_url(url) - if self.catalogs: - self.save_cache() + """ + Adds catalog to the manager by its url. + :param url: url of the catalog which to load + :return: The catalog + :rtype: :py:class: `~king_phisher.Catalog` + """ + catalog_ = super(ClientCatalogManager, self).add_catalog_url(url) + if catalog_: + self.save_cache(catalog=catalog_) + return catalog_ def is_compatible(self, catalog_id, repo_id, plugin_name): + """ + checks the compatibility of a plugin + :param catalog_id: The catalog id associated with the plugin. + :param repo_id: The repository id associated with the plugin. + :param plugin_name: The name of the plugin + :return: If it is compatible + :rtype: bool + """ plugin = self.get_collection(catalog_id, repo_id)[plugin_name] return Requirements(plugin['requirements']).is_compatible def compatibility(self, catalog_id, repo_id, plugin_name): + """ + checks the compatibility of a plugin + :param catalog_id: The catalog id associated with the plugin. + :param repo_id: The repository id associated with the plugin. + :param plugin_name: The name of the plugin + :return: List of packages and if the requirements are meet. + :rtype: tuple + """ plugin = self.get_collection(catalog_id, repo_id)[plugin_name] return Requirements(plugin['requirements']).compatibility def get_collections_to_cache(self, catalog_): + """ + Will create a list of repositories and its collections in the accurate format to send to cache + :param catalog_: The :py:class: '~king_phisher.Catalog` + :return: the repository cache information + :rtype: list + """ repo_cache_info = [] for repo in self.get_repositories(catalog_): repo_cache_info.append({ @@ -673,4 +719,9 @@ def get_collections_to_cache(self, catalog_): return repo_cache_info def get_cache(self): + """ + Gets the catalog cache + :return: The catalog cache + :rtype: :py:class:`.CatalogCacheManager` + """ return self._catalog_cache diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 0626949d..c4eb30e8 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -58,8 +58,15 @@ class PluginManagerWindow(gui_utilities.GladeGObject): """ dependencies = gui_utilities.GladeDependencies( children=( - 'expander_plugin_info', + 'expander_info', + 'grid_catalog_repo_info', 'grid_plugin_info', + 'label_catalog_repo_info_title', + 'label_catalog_repo_info_description', + 'label_catalog_repo_info_for_description', + 'label_catalog_repo_info_homepage', + 'label_catalog_repo_info_maintainers', + 'label_catalog_repo_info_for_maintainers', 'label_plugin_info_authors', 'label_plugin_info_for_compatible', 'label_plugin_info_compatible', @@ -69,10 +76,10 @@ class PluginManagerWindow(gui_utilities.GladeGObject): 'label_plugin_info_version', 'paned_plugins', 'scrolledwindow_plugins', - 'stack_plugin_info', + 'stack_info', 'treeview_plugins', 'textview_plugin_info', - 'viewport_plugin_info', + 'viewport_info', 'statusbar' ) ) @@ -151,7 +158,7 @@ def signal_window_show(self, _): def _load_catalogs(self): self._update_status_bar('Loading: Downloading Catalogs', idle=True) - self.catalog_plugins = ClientCatalogManager() + self.catalog_plugins = ClientCatalogManager(application.USER_DATA_PATH) for catalog in self.config['catalogs']: self.logger.debug("downloading catalog: {}".format(catalog)) self._update_status_bar("Loading: Downloading Catalog: {}".format(catalog)) @@ -234,16 +241,20 @@ def _load_plugins(self): repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog, model) plugin_collections = self.catalog_plugins.get_collection(catalog_id, repos.id) self._add_plugins_to_tree(catalog_id, repos, store, repo_line, plugin_collections) - else: - if not self.config['plugins.installed']: - return - for catalog_id, repositories in self.catalog_plugins.get_cache().items(): - model = (catalog_id, None, True, "{} (offline)".format(catalog_id), None, None, False, False, False, _ROW_TYPE_CATALOG) - catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) - for repo in repositories: - model = (repo.id, None, True, "{} (offline)".format(repo.title), None, None, False, False, False, _ROW_TYPE_REPOSITORY) - repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) - self._add_plugins_offline(catalog_id, repo.id, store, repo_line) + + if not self.config['plugins.installed']: + return + catalog_cache = self.catalog_plugins.get_cache() + for catalog_id in catalog_cache: + if self.catalog_plugins.catalogs.get(catalog_id, None): + continue + named_catalog = catalog_cache[catalog_id] + model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) + catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) + for repo in named_catalog.repositories: + model = (repo.id, None, True, repo.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) + repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) + self._add_plugins_offline(catalog_id, repo.id, store, repo_line) gui_utilities.glib_idle_add_once(self._treeview_unselect) @@ -254,9 +265,10 @@ def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): installed = False enabled = False if plugin_list[plugin]['name'] in self.config['plugins.installed']: - if repo.id == self.config['plugins.installed'][plugin_list[plugin]['name']][1]: - installed = True - enabled = plugin_list[plugin]['name'] in self.config['plugins.enabled'] + if repo.id == self.config['plugins.installed'][plugin_list[plugin]['name']]['repo_id']: + if catalog == self.config['plugins.installed'][plugin_list[plugin]['name']]['catalog_id']: + installed = True + enabled = plugin_list[plugin]['name'] in self.config['plugins.enabled'] models.append(self._named_model( id=plugin, installed=installed, @@ -274,9 +286,11 @@ def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): def _add_plugins_offline(self, catalog_id, repo_id, store, parent): models = [] for plugin in self.config['plugins.installed']: - if self.config['plugins.installed'][plugin][0] != catalog_id: + if plugin not in self.application.plugin_manager: continue - if self.config['plugins.installed'][plugin][1] != repo_id: + if self.config['plugins.installed'][plugin]['catalog_id'] != catalog_id: + continue + if self.config['plugins.installed'][plugin]['repo_id'] != repo_id: continue models.append(self._named_model( id=plugin, @@ -300,8 +314,9 @@ def _get_version_or_upgrade(self, plugin_name, plugin_version): return self.application.plugin_manager[plugin_name].version def signal_popup_menu_activate_reload_all(self, _): - self.load_thread = utilities.Thread(target=self._load_plugins) - self.load_thread.start() + if not self.load_thread.isAlive(): + self.load_thread = utilities.Thread(target=self._load_catalogs) + self.load_thread.start() def signal_destory(self, _): if self.catalog_plugins: @@ -362,7 +377,7 @@ def signal_expander_activate(self, expander): paned.set_position(paned.get_allocation().height + self._paned_offset) def signal_paned_button_press_event(self, paned, event): - return not self.gobjects['expander_plugin_info'].get_property('expanded') + return not self.gobjects['expander_info'].get_property('expanded') def signal_popup_menu_activate_reload(self, _): treeview = self.gobjects['treeview_plugins'] @@ -402,10 +417,10 @@ def signal_renderer_toggled_enable(self, _, path): return if named_row.id not in pm.loaded_plugins: return - if self._model[path].parent: + if self._named_model(*self._model[path].parent).id: installed_plugin_info = self.config['plugins.installed'][named_row.id] repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) - if repo_model.id != installed_plugin_info[1] or catalog_model.id != installed_plugin_info[0]: + if repo_model.id != installed_plugin_info['repo_id'] or catalog_model.id != installed_plugin_info['catalog_id']: return if named_row.id in self._module_errors: gui_utilities.show_dialog_error('Can Not Enable Plugin', self.window, 'Can not enable a plugin which failed to load.') @@ -440,12 +455,12 @@ def signal_renderer_toggled_install(self, _, path): else: if named_row.id in self.config['plugins.installed']: installed_plugin_info = self.config['plugins.installed'][named_row.id] - if installed_plugin_info != [catalog_model.id, repo_model.id]: + if installed_plugin_info != {'catalog_id': catalog_model.id, 'repo_id': repo_model.id, 'plugin_id': named_row.id}: window_question = "A plugin with this name is already installed from Catalog: {} Repo: {}\nDo you want to replace it with this one?" response = gui_utilities.show_dialog_yes_no( 'Plugin installed from another source', self.window, - window_question.format(installed_plugin_info[0], installed_plugin_info[1]) + window_question.format(installed_plugin_info['catalog_id'], installed_plugin_info['repo_id']) ) if not response: return @@ -453,7 +468,7 @@ def signal_renderer_toggled_install(self, _, path): self.logger.warning('failed to uninstall plugin {}'.format(named_row.id)) return self.catalog_plugins.install_plugin(catalog_model.id, repo_model.id, named_row.id, self.plugin_path) - self.config['plugins.installed'][named_row.id] = [catalog_model.id, repo_model.id] + self.config['plugins.installed'][named_row.id] = {'catalog_id': catalog_model.id, 'repo_id': repo_model.id, 'plugin_id': named_row.id} self._set_model_item(path, 'installed', True) self._set_model_item(path, 'version', self.catalog_plugins.get_collection(catalog_model.id, repo_model.id)[named_row.id]['version']) self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(named_row.id, catalog_model.id, repo_model.id)) @@ -471,10 +486,10 @@ def _disable_plugin(self, path, is_path=True): def _remove_matching_plugin(self, path, installed_plugin_info): named_row = self._named_model(*self._model[path]) for catalog_model in self._model: - if self._named_model(*catalog_model).id != installed_plugin_info[0]: + if self._named_model(*catalog_model).id != installed_plugin_info['catalog_id']: continue for repo_model in catalog_model.iterchildren(): - if self._named_model(*repo_model).id != installed_plugin_info[1]: + if self._named_model(*repo_model).id != installed_plugin_info['repo_id']: continue for plugin_model in repo_model.iterchildren(): named_model = self._named_model(*plugin_model) @@ -483,7 +498,7 @@ def _remove_matching_plugin(self, path, installed_plugin_info): if named_model.enabled: self._disable_plugin(plugin_model, is_path=False) self._uninstall_plugin( - self.catalog_plugins.get_collection(installed_plugin_info[0], installed_plugin_info[1]), + self.catalog_plugins.get_collection(installed_plugin_info['catalog_id'], installed_plugin_info['repo_id']), plugin_model, is_path=False ) @@ -507,7 +522,7 @@ def _uninstall_plugin(self, plugin_collection, path, is_path=True): def _set_plugin_info(self, model_instance): named_model = self._named_model(*model_instance) - stack = self.gobjects['stack_plugin_info'] + stack = self.gobjects['stack_info'] textview = self.gobjects['textview_plugin_info'] buf = textview.get_buffer() buf.delete(buf.get_start_iter(), buf.get_end_iter()) @@ -523,18 +538,45 @@ def _set_plugin_info(self, model_instance): self._set_non_plugin_info(model_instance) def _set_non_plugin_info(self, model_instance): + stack = self.gobjects['stack_info'] + stack.set_visible_child(self.gobjects['grid_catalog_repo_info']) named_model = self._named_model(*model_instance) - self.gobjects['label_plugin_info_homepage'].set_property('visible', False) - self.gobjects['label_plugin_info_title'].set_text(named_model.title) - if named_model.title.lower().endswith(' (offline)') or (named_model.id is None and named_model.type == _ROW_TYPE_CATALOG): + obj_catalog = None + self._hide_catalog_repo_lables() + self.gobjects['label_catalog_repo_info_title'].set_text(named_model.title) + if not named_model.id: return if named_model.type == _ROW_TYPE_CATALOG: - obj = self.catalog_plugins.catalogs[named_model.id] + obj = self.catalog_plugins.catalogs.get(named_model.id, None) + if not obj: + return else: + obj_catalog = self.catalog_plugins.catalogs.get(self._named_model(*model_instance.parent).id, None) + if not obj_catalog: + return obj = self.catalog_plugins.catalogs[self._named_model(*model_instance.parent).id].repositories[named_model.id] + + maintainers = getattr(obj, 'maintainers', getattr(obj_catalog, 'maintainers', None)) + if maintainers: + self.gobjects['label_catalog_repo_info_maintainers'].set_text('\n'.join(maintainers)) + self.gobjects['label_catalog_repo_info_maintainers'].set_property('visible', True) + self.gobjects['label_catalog_repo_info_for_maintainers'].set_property('visible', True) if getattr(obj, 'description', None): - self.gobjects['label_plugin_info_description'].set_text(obj.description) - self._set_homepage_url(getattr(obj, 'homepage', None)) + self.gobjects['label_catalog_repo_info_description'].set_text(obj.description) + self.gobjects['label_catalog_repo_info_description'].set_property('visible', True) + self.gobjects['label_catalog_repo_info_for_description'].set_property('visible', True) + if getattr(obj, 'homepage', None) or getattr(obj, 'url', None): + url = getattr(obj, 'homepage', getattr(obj, 'url', None)) + self.gobjects['label_catalog_repo_info_homepage'].set_markup("Homepage".format(url)) + self.gobjects['label_catalog_repo_info_homepage'].set_property('tooltip-text', url) + self.gobjects['label_catalog_repo_info_homepage'].set_property('visible', True) + + def _hide_catalog_repo_lables(self): + self.gobjects['label_catalog_repo_info_maintainers'].set_property('visible', False) + self.gobjects['label_catalog_repo_info_for_maintainers'].set_property('visible', False) + self.gobjects['label_catalog_repo_info_description'].set_property('visible', False) + self.gobjects['label_catalog_repo_info_for_description'].set_property('visible', False) + self.gobjects['label_catalog_repo_info_homepage'].set_property('visible', False) def _set_plugin_info_details(self, plugin_model): named_model = self._named_model(*plugin_model) From a615ebee262e9c6def46f52a4167ee908dc2ece1 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 6 Nov 2017 21:41:13 -0500 Subject: [PATCH 17/38] Code tweaks and doc string fixes --- docs/source/king_phisher/client/plugins.rst | 6 +++ king_phisher/catalog.py | 9 ++-- king_phisher/client/plugins.py | 50 +++++++++++-------- king_phisher/client/windows/plugin_manager.py | 22 ++++---- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/docs/source/king_phisher/client/plugins.rst b/docs/source/king_phisher/client/plugins.rst index a03e7f54..17d639a0 100644 --- a/docs/source/king_phisher/client/plugins.rst +++ b/docs/source/king_phisher/client/plugins.rst @@ -7,6 +7,12 @@ Classes ------- +.. autoclass:: king_phisher.client.plugins.ClientCatalogManager + :show-inheritance: + :members: + :inherited-members: + :special-members: __init__ + .. autoclass:: king_phisher.client.plugins.ClientOptionBoolean :show-inheritance: :members: diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index 9172e196..d994b859 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -362,7 +362,7 @@ def catalog_ids(self): """ The key names of the catalogs in the manager - :return: the catalogs ids in CatalogManager + :return: The catalogs IDs in the manager instance. :rtype: dict_keys """ return self.catalogs.keys() @@ -378,9 +378,10 @@ def get_repositories(self, catalog_id): def add_catalog_url(self, url): """ - Adds catalog to the manager by its url. - :param url: url of the catalog which to load - :return: The catalog + Adds catalog to the manager by its URL. + + :param str url: The URL of the catalog to load. + :return: The catalog. :rtype: :py:class: `.Catalog` """ try: diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index 0c4cde20..c0a9c865 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -42,7 +42,6 @@ from king_phisher.client import gui_utilities from king_phisher.client import mailer from king_phisher.client.widget import extras -from king_phisher.plugins import Requirements from king_phisher.serializers import JSON from gi.repository import Gtk @@ -573,7 +572,7 @@ def __init__(self, cache_file): self._cache_dict = {} if self._cache_dict and 'catalogs' in self._cache_dict: - cache_cat = self._cache_dict['catalogs']['values'] + cache_cat = self._cache_dict['catalogs']['value'] for catalog_ in cache_cat: self[catalog_] = self._CatalogCacheEntry( cache_cat[catalog_]['id'], @@ -612,8 +611,8 @@ def to_dict(self): return cache def save(self): - self._cache_dict['catalogs']['timestamp'] = datetime.datetime.utcnow() - self._cache_dict['catalogs']['values'] = self.to_dict() + self._cache_dict['catalogs']['created'] = datetime.datetime.utcnow() + self._cache_dict['catalogs']['value'] = self.to_dict() with open(self._cache_file, 'w') as file_h: JSON.dump(self._cache_dict, file_h) @@ -640,6 +639,7 @@ def get_collection(self, catalog_id, repo_id): def install_plugin(self, catalog_id, repo_id, plugin_id, install_path): """ Installs the specified plugin to the desired plugin path. + :param catalog_id: The id of the catalog of the desired plugin to install. :param repo_id: The id of the repository of the desired plugin to install. :param plugin_id: The id of the plugin to install. @@ -649,8 +649,9 @@ def install_plugin(self, catalog_id, repo_id, plugin_id, install_path): def save_cache(self, catalog=None): """ - saves the catalog or catalogs in the manager to the cache - :param catalog: the :py:class: `~king_phisher.Catalog` to save + Saves the catalog or catalogs in the manager to the cache. + + :param catalog: The :py:class:`~king_phisher.catalog.Catalog` to save. """ if catalog: self._catalog_cache.cache_catalog_repositories( @@ -668,9 +669,10 @@ def save_cache(self, catalog=None): def add_catalog_url(self, url): """ Adds catalog to the manager by its url. - :param url: url of the catalog which to load - :return: The catalog - :rtype: :py:class: `~king_phisher.Catalog` + + :param url: URL of the catalog to load. + :return: The catalog. + :rtype: :py:class:`~king_phisher.catalog.Catalog` """ catalog_ = super(ClientCatalogManager, self).add_catalog_url(url) if catalog_: @@ -679,33 +681,36 @@ def add_catalog_url(self, url): def is_compatible(self, catalog_id, repo_id, plugin_name): """ - checks the compatibility of a plugin + Checks the compatibility of a plugin. + :param catalog_id: The catalog id associated with the plugin. :param repo_id: The repository id associated with the plugin. - :param plugin_name: The name of the plugin - :return: If it is compatible + :param plugin_name: The name of the plugin. + :return: Whether or not it is compatible. :rtype: bool """ plugin = self.get_collection(catalog_id, repo_id)[plugin_name] - return Requirements(plugin['requirements']).is_compatible + return plugins.Requirements(plugin['requirements']).is_compatible def compatibility(self, catalog_id, repo_id, plugin_name): """ - checks the compatibility of a plugin + Checks the compatibility of a plugin. + :param catalog_id: The catalog id associated with the plugin. :param repo_id: The repository id associated with the plugin. - :param plugin_name: The name of the plugin - :return: List of packages and if the requirements are meet. + :param plugin_name: The name of the plugin. + :return: Tuple of packages and if the requirements are met. :rtype: tuple """ plugin = self.get_collection(catalog_id, repo_id)[plugin_name] - return Requirements(plugin['requirements']).compatibility + return plugins.Requirements(plugin['requirements']).compatibility def get_collections_to_cache(self, catalog_): """ - Will create a list of repositories and its collections in the accurate format to send to cache - :param catalog_: The :py:class: '~king_phisher.Catalog` - :return: the repository cache information + Will create a list of repositories and its collections in the accurate format to send to cache. + + :param catalog_: The :py:class:`~king_phisher.catalog.Catalog` instance. + :return: The repository cache information. :rtype: list """ repo_cache_info = [] @@ -720,8 +725,9 @@ def get_collections_to_cache(self, catalog_): def get_cache(self): """ - Gets the catalog cache - :return: The catalog cache + Gets the catalog cache. + + :return: The catalog cache. :rtype: :py:class:`.CatalogCacheManager` """ return self._catalog_cache diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index c4eb30e8..9e5f6f3a 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -314,7 +314,7 @@ def _get_version_or_upgrade(self, plugin_name, plugin_version): return self.application.plugin_manager[plugin_name].version def signal_popup_menu_activate_reload_all(self, _): - if not self.load_thread.isAlive(): + if not self.load_thread.is_alive(): self.load_thread = utilities.Thread(target=self._load_catalogs) self.load_thread.start() @@ -323,7 +323,7 @@ def signal_destory(self, _): self.catalog_plugins.save_cache() def signal_treeview_row_activated(self, treeview, path, column): - self._set_plugin_info(self._model[path]) + self._set_info(self._model[path]) def signal_label_activate_link(self, _, uri): utilities.open_uri(uri) @@ -399,14 +399,14 @@ def signal_popup_menu_activate_reload(self, _): except Exception as error: self._on_plugin_load_error(named_row.id, error) if named_row.id == selected_plugin: - self._set_plugin_info(named_row.id) + self._set_info(named_row.id) self._set_model_item(tree_iter, 'title', "{0} (Reload Failed)".format(named_row.id)) continue if named_row.id in self._module_errors: del self._module_errors[named_row.id] self._set_model_item(tree_iter, 'title', klass.title) if named_row.id == selected_plugin: - self._set_plugin_info(named_row.id) + self._set_info(named_row.id) if enabled: pm.enable(named_row.id) @@ -520,7 +520,7 @@ def _uninstall_plugin(self, plugin_collection, path, is_path=True): else: path[self._named_model._fields.index('installed')] = False - def _set_plugin_info(self, model_instance): + def _set_info(self, model_instance): named_model = self._named_model(*model_instance) stack = self.gobjects['stack_info'] textview = self.gobjects['textview_plugin_info'] @@ -533,16 +533,16 @@ def _set_plugin_info(self, model_instance): self._set_plugin_info_error(model_id) else: stack.set_visible_child(self.gobjects['grid_plugin_info']) - self._set_plugin_info_details(model_instance) + self._set_plugin_info(model_instance) else: - self._set_non_plugin_info(model_instance) + self._set_nonplugin_info(model_instance) - def _set_non_plugin_info(self, model_instance): + def _set_nonplugin_info(self, model_instance): stack = self.gobjects['stack_info'] stack.set_visible_child(self.gobjects['grid_catalog_repo_info']) named_model = self._named_model(*model_instance) obj_catalog = None - self._hide_catalog_repo_lables() + self._hide_catalog_repo_labels() self.gobjects['label_catalog_repo_info_title'].set_text(named_model.title) if not named_model.id: return @@ -571,14 +571,14 @@ def _set_non_plugin_info(self, model_instance): self.gobjects['label_catalog_repo_info_homepage'].set_property('tooltip-text', url) self.gobjects['label_catalog_repo_info_homepage'].set_property('visible', True) - def _hide_catalog_repo_lables(self): + def _hide_catalog_repo_labels(self): self.gobjects['label_catalog_repo_info_maintainers'].set_property('visible', False) self.gobjects['label_catalog_repo_info_for_maintainers'].set_property('visible', False) self.gobjects['label_catalog_repo_info_description'].set_property('visible', False) self.gobjects['label_catalog_repo_info_for_description'].set_property('visible', False) self.gobjects['label_catalog_repo_info_homepage'].set_property('visible', False) - def _set_plugin_info_details(self, plugin_model): + def _set_plugin_info(self, plugin_model): named_model = self._named_model(*plugin_model) pm = self.application.plugin_manager self._last_plugin_selected = plugin_model From 42a42d91e60ba1bf1810fd742817b8cfbea0b9a0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 6 Nov 2017 22:13:06 -0500 Subject: [PATCH 18/38] Update the plugin manager status messages --- king_phisher/client/windows/plugin_manager.py | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 9e5f6f3a..66a4aa1d 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -151,17 +151,16 @@ def __init__(self, *args, **kwargs): def _treeview_unselect(self): treeview = self.gobjects['treeview_plugins'] treeview.get_selection().unselect_all() - self._update_status_bar('Finished Loading Plugin Store', idle=True) def signal_window_show(self, _): pass def _load_catalogs(self): - self._update_status_bar('Loading: Downloading Catalogs', idle=True) + self._update_status_bar('Loading, downloading catalogs...', idle=True) self.catalog_plugins = ClientCatalogManager(application.USER_DATA_PATH) for catalog in self.config['catalogs']: self.logger.debug("downloading catalog: {}".format(catalog)) - self._update_status_bar("Loading: Downloading Catalog: {}".format(catalog)) + self._update_status_bar("Loading, downloading catalog: {}".format(catalog)) self.catalog_plugins.add_catalog_url(catalog) self._load_plugins() @@ -200,7 +199,7 @@ def _load_plugins(self): visible to the user. """ self.logger.debug('loading plugins') - self._update_status_bar('Loading... Plugins...', idle=True) + self._update_status_bar('Loading plugins...', idle=True) store = self._model store.clear() pm = self.application.plugin_manager @@ -242,21 +241,21 @@ def _load_plugins(self): plugin_collections = self.catalog_plugins.get_collection(catalog_id, repos.id) self._add_plugins_to_tree(catalog_id, repos, store, repo_line, plugin_collections) - if not self.config['plugins.installed']: - return - catalog_cache = self.catalog_plugins.get_cache() - for catalog_id in catalog_cache: - if self.catalog_plugins.catalogs.get(catalog_id, None): - continue - named_catalog = catalog_cache[catalog_id] - model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) - catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) - for repo in named_catalog.repositories: - model = (repo.id, None, True, repo.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) - repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) - self._add_plugins_offline(catalog_id, repo.id, store, repo_line) + if self.config['plugins.installed']: + catalog_cache = self.catalog_plugins.get_cache() + for catalog_id in catalog_cache: + if self.catalog_plugins.catalogs.get(catalog_id, None): + continue + named_catalog = catalog_cache[catalog_id] + model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) + catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) + for repo in named_catalog.repositories: + model = (repo.id, None, True, repo.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) + repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) + self._add_plugins_offline(catalog_id, repo.id, store, repo_line) gui_utilities.glib_idle_add_once(self._treeview_unselect) + self._update_status_bar('Loading completed', idle=True) def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): client_plugins = list(plugin_list) From 290e2f160c7d580ea401ba2126d4590ae53172f7 Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Tue, 7 Nov 2017 15:45:43 -0500 Subject: [PATCH 19/38] Fixed so you can not reload non-installed plugins --- king_phisher/client/windows/plugin_manager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 66a4aa1d..bb2b0b13 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -126,7 +126,7 @@ def __init__(self, *args, **kwargs): tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 7) tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'sensitive', 8) self._model = Gtk.TreeStore(str, bool, bool, str, str, str, bool, bool, bool, str) - self._model.set_sort_column_id(1, Gtk.SortType.DESCENDING) + self._model.set_sort_column_id(3, Gtk.SortType.ASCENDING) treeview.set_model(self._model) self.plugin_path = os.path.join(application.USER_DATA_PATH, 'plugins') self.load_thread = utilities.Thread(target=self._load_catalogs) @@ -313,7 +313,7 @@ def _get_version_or_upgrade(self, plugin_name, plugin_version): return self.application.plugin_manager[plugin_name].version def signal_popup_menu_activate_reload_all(self, _): - if not self.load_thread.is_alive(): + if not self.load_thread.isAlive(): self.load_thread = utilities.Thread(target=self._load_catalogs) self.load_thread.start() @@ -391,6 +391,8 @@ def signal_popup_menu_activate_reload(self, _): named_row = self._named_model(*self._model[tree_iter]) if named_row.type != _ROW_TYPE_PLUGIN: continue + if named_row.id not in pm.enabled_plugins: + continue enabled = named_row.id in pm.enabled_plugins pm.unload(named_row.id) try: @@ -405,7 +407,7 @@ def signal_popup_menu_activate_reload(self, _): del self._module_errors[named_row.id] self._set_model_item(tree_iter, 'title', klass.title) if named_row.id == selected_plugin: - self._set_info(named_row.id) + self._set_info(self._model[tree_iter]) if enabled: pm.enable(named_row.id) @@ -529,7 +531,7 @@ def _set_info(self, model_instance): if named_model.type == _ROW_TYPE_PLUGIN: if model_id in self._module_errors: stack.set_visible_child(textview) - self._set_plugin_info_error(model_id) + self._set_plugin_info_error(model_instance) else: stack.set_visible_child(self.gobjects['grid_plugin_info']) self._set_plugin_info(model_instance) From cdd5e6ac7f76704602c0c4eba86dd747ce3ecdfb Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Tue, 7 Nov 2017 16:53:26 -0500 Subject: [PATCH 20/38] Changed enable sensitive to installed, [1] of the TreeModel --- king_phisher/client/windows/plugin_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index bb2b0b13..e3b74bab 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -122,7 +122,7 @@ def __init__(self, *args, **kwargs): ) tvm.column_views['Enabled'].set_cell_data_func(toggle_renderer_enable, self._toggle_cell_data_func) tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'visible', 6) - tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'sensitive', 7) + tvm.column_views['Enabled'].add_attribute(toggle_renderer_enable, 'sensitive', 1) tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'visible', 7) tvm.column_views['Installed'].add_attribute(toggle_renderer_install, 'sensitive', 8) self._model = Gtk.TreeStore(str, bool, bool, str, str, str, bool, bool, bool, str) @@ -313,7 +313,7 @@ def _get_version_or_upgrade(self, plugin_name, plugin_version): return self.application.plugin_manager[plugin_name].version def signal_popup_menu_activate_reload_all(self, _): - if not self.load_thread.isAlive(): + if not self.load_thread.is_alive(): self.load_thread = utilities.Thread(target=self._load_catalogs) self.load_thread.start() From e4bd06c9cff181b35b5e5efc100d5af890387552 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 7 Nov 2017 23:02:26 -0500 Subject: [PATCH 21/38] Fix how locally installed plugins are handled --- king_phisher/client/windows/plugin_manager.py | 209 +++++++++--------- 1 file changed, 106 insertions(+), 103 deletions(-) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index e3b74bab..6127808e 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -32,6 +32,7 @@ import collections import os +import shutil import sys import traceback @@ -49,6 +50,8 @@ _ROW_TYPE_PLUGIN = 'plugin' _ROW_TYPE_REPOSITORY = 'repository' _ROW_TYPE_CATALOG = 'catalog' +_LOCAL_REPOSITORY_ID = 'local' +_LOCAL_REPOSITORY_TITLE = '[Locally Installed]' class PluginManagerWindow(gui_utilities.GladeGObject): """ @@ -205,12 +208,13 @@ def _load_plugins(self): pm = self.application.plugin_manager self._module_errors = {} pm.load_all(on_error=self._on_plugin_load_error) - model = (None, None, True, '[Locally Installed]', None, None, False, False, False, _ROW_TYPE_CATALOG) - catalog = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) + model = (_LOCAL_REPOSITORY_ID, None, True, _LOCAL_REPOSITORY_TITLE, None, None, False, False, False, _ROW_TYPE_CATALOG) + catalog_row = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) models = [] for name, plugin in pm.loaded_plugins.items(): - if name in self.config['plugins.installed']: + if self.config['plugins.installed'].get(name): continue + self.config['plugins.installed'][name] = None models.append(self._named_model( id=plugin.name, installed=True, @@ -223,58 +227,56 @@ def _load_plugins(self): sensitive_installed=False, type=_ROW_TYPE_PLUGIN )) - gui_utilities.glib_idle_add_once(self._store_append, store, catalog, models[-1]) + gui_utilities.glib_idle_add_once(self._store_extend, store, catalog_row, models) del models for name in self._module_errors.keys(): - model = (name, True, False, "{0} (Load Failed)".format(name), 'unknown', True, True, False, _ROW_TYPE_PLUGIN) - gui_utilities.glib_idle_add_once(self._store_append, store, catalog, model) + model = (name, True, False, "{0} (Load Failed)".format(name), 'No', 'Unknown', True, True, False, _ROW_TYPE_PLUGIN) + gui_utilities.glib_idle_add_once(self._store_append, store, catalog_row, model) self.logger.debug('loading catalog into plugin treeview') - if self.catalog_plugins.catalog_ids(): - for catalog_id in self.catalog_plugins.catalog_ids(): - model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) - catalog = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) - for repos in self.catalog_plugins.get_repositories(catalog_id): - model = (repos.id, None, True, repos.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) - repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog, model) - plugin_collections = self.catalog_plugins.get_collection(catalog_id, repos.id) - self._add_plugins_to_tree(catalog_id, repos, store, repo_line, plugin_collections) - - if self.config['plugins.installed']: - catalog_cache = self.catalog_plugins.get_cache() - for catalog_id in catalog_cache: - if self.catalog_plugins.catalogs.get(catalog_id, None): - continue - named_catalog = catalog_cache[catalog_id] - model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) - catalog_line = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) - for repo in named_catalog.repositories: - model = (repo.id, None, True, repo.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) - repo_line = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_line, model) - self._add_plugins_offline(catalog_id, repo.id, store, repo_line) + for catalog_id in self.catalog_plugins.catalog_ids(): + model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) + catalog_row = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) + for repo in self.catalog_plugins.get_repositories(catalog_id): + model = (repo.id, None, True, repo.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) + repo_row = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_row, model) + plugin_collections = self.catalog_plugins.get_collection(catalog_id, repo.id) + self._add_plugins_to_tree(catalog_id, repo, store, repo_row, plugin_collections) + + catalog_cache = self.catalog_plugins.get_cache() + for catalog_id in catalog_cache: + if self.catalog_plugins.catalogs.get(catalog_id, None): + continue + named_catalog = catalog_cache[catalog_id] + model = (catalog_id, None, True, catalog_id, None, None, False, False, False, _ROW_TYPE_CATALOG) + catalog_row = gui_utilities.glib_idle_add_wait(self._store_append, store, None, model) + for repo in named_catalog.repositories: + model = (repo.id, None, True, repo.title, None, None, False, False, False, _ROW_TYPE_REPOSITORY) + repo_row = gui_utilities.glib_idle_add_wait(self._store_append, store, catalog_row, model) + self._add_plugins_offline(catalog_id, repo.id, store, repo_row) gui_utilities.glib_idle_add_once(self._treeview_unselect) self._update_status_bar('Loading completed', idle=True) - def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): + def _add_plugins_to_tree(self, catalog_id, repo, store, parent, plugin_list): client_plugins = list(plugin_list) models = [] for plugin in client_plugins: installed = False enabled = False - if plugin_list[plugin]['name'] in self.config['plugins.installed']: - if repo.id == self.config['plugins.installed'][plugin_list[plugin]['name']]['repo_id']: - if catalog == self.config['plugins.installed'][plugin_list[plugin]['name']]['catalog_id']: - installed = True - enabled = plugin_list[plugin]['name'] in self.config['plugins.enabled'] + plugin_name = plugin_list[plugin]['name'] + install_src = self.config['plugins.installed'].get(plugin_name) + if install_src and repo.id == install_src['repo_id'] and catalog_id == install_src['catalog_id']: + installed = True + enabled = plugin_name in self.config['plugins.enabled'] models.append(self._named_model( id=plugin, installed=installed, enabled=enabled, title=plugin_list[plugin]['title'], - compatibility='Yes' if self.catalog_plugins.is_compatible(catalog, repo.id, plugin) else 'No', - version=self._get_version_or_upgrade(plugin_list[plugin]['name'], plugin_list[plugin]['version']), + compatibility='Yes' if self.catalog_plugins.is_compatible(catalog_id, repo.id, plugin) else 'No', + version=self._get_version_or_upgrade(plugin_name, plugin_list[plugin]['version']), visible_enabled=True, visible_installed=True, sensitive_installed=True, @@ -284,20 +286,22 @@ def _add_plugins_to_tree(self, catalog, repo, store, parent, plugin_list): def _add_plugins_offline(self, catalog_id, repo_id, store, parent): models = [] - for plugin in self.config['plugins.installed']: - if plugin not in self.application.plugin_manager: + for plugin_name, plugin_src in self.config['plugins.installed'].items(): + if plugin_src is None: + continue + if plugin_name not in self.application.plugin_manager: continue - if self.config['plugins.installed'][plugin]['catalog_id'] != catalog_id: + if plugin_src['catalog_id'] != catalog_id: continue - if self.config['plugins.installed'][plugin]['repo_id'] != repo_id: + if plugin_src['repo_id'] != repo_id: continue models.append(self._named_model( - id=plugin, + id=plugin_name, installed=True, - enabled=plugin in self.config['plugins.enabled'], - title=self.application.plugin_manager[plugin].title, - compatibility='Yes' if self.application.plugin_manager[plugin].is_compatible else 'No', - version=self.application.plugin_manager[plugin].version, + enabled=plugin_name in self.config['plugins.enabled'], + title=self.application.plugin_manager[plugin_name].title, + compatibility='Yes' if self.application.plugin_manager[plugin_name].is_compatible else 'No', + version=self.application.plugin_manager[plugin_name].version, visible_enabled=True, visible_installed=True, sensitive_installed=False, @@ -419,9 +423,9 @@ def signal_renderer_toggled_enable(self, _, path): if named_row.id not in pm.loaded_plugins: return if self._named_model(*self._model[path].parent).id: - installed_plugin_info = self.config['plugins.installed'][named_row.id] + plugin_src = self.config['plugins.installed'][named_row.id] repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) - if repo_model.id != installed_plugin_info['repo_id'] or catalog_model.id != installed_plugin_info['catalog_id']: + if plugin_src and (repo_model.id != plugin_src['repo_id'] or catalog_model.id != plugin_src['catalog_id']): return if named_row.id in self._module_errors: gui_utilities.show_dialog_error('Can Not Enable Plugin', self.window, 'Can not enable a plugin which failed to load.') @@ -439,40 +443,29 @@ def signal_renderer_toggled_enable(self, _, path): def signal_renderer_toggled_install(self, _, path): repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) - plugin_collection = self.catalog_plugins.get_collection(catalog_model.id, repo_model.id) named_row = self._named_model(*self._model[path]) if named_row.installed: if named_row.enabled: - response = gui_utilities.show_dialog_yes_no( - 'Plugin is enabled', - self.window, - "This will disable the plugin, do you wish to continue?" - ) - if not response: + if not gui_utilities.show_dialog_yes_no('Plugin is enabled', self.window, 'This will disable the plugin, do you want to continue?'): return self._disable_plugin(path) - self._uninstall_plugin(plugin_collection, path) - self.logger.info("uninstalled plugin {}".format(named_row.id)) - else: - if named_row.id in self.config['plugins.installed']: - installed_plugin_info = self.config['plugins.installed'][named_row.id] - if installed_plugin_info != {'catalog_id': catalog_model.id, 'repo_id': repo_model.id, 'plugin_id': named_row.id}: - window_question = "A plugin with this name is already installed from Catalog: {} Repo: {}\nDo you want to replace it with this one?" - response = gui_utilities.show_dialog_yes_no( - 'Plugin installed from another source', - self.window, - window_question.format(installed_plugin_info['catalog_id'], installed_plugin_info['repo_id']) - ) - if not response: - return - if not self._remove_matching_plugin(path, installed_plugin_info): - self.logger.warning('failed to uninstall plugin {}'.format(named_row.id)) - return - self.catalog_plugins.install_plugin(catalog_model.id, repo_model.id, named_row.id, self.plugin_path) - self.config['plugins.installed'][named_row.id] = {'catalog_id': catalog_model.id, 'repo_id': repo_model.id, 'plugin_id': named_row.id} - self._set_model_item(path, 'installed', True) - self._set_model_item(path, 'version', self.catalog_plugins.get_collection(catalog_model.id, repo_model.id)[named_row.id]['version']) - self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(named_row.id, catalog_model.id, repo_model.id)) + self._uninstall_plugin(path) + return + + if named_row.id in self.config['plugins.installed']: + plugin_src = self.config['plugins.installed'].get(named_row.id) + if plugin_src != {'catalog_id': catalog_model.id, 'repo_id': repo_model.id, 'plugin_id': named_row.id}: + window_question = 'A plugin with this name is already installed from another\nrepository. Do you want to replace it with this one?' + if not gui_utilities.show_dialog_yes_no('Plugin installed from another source', self.window, window_question): + return + if not self._remove_matching_plugin(path, plugin_src): + self.logger.warning("failed to uninstall plugin {0}".format(named_row.id)) + return + self.catalog_plugins.install_plugin(catalog_model.id, repo_model.id, named_row.id, self.plugin_path) + self.config['plugins.installed'][named_row.id] = {'catalog_id': catalog_model.id, 'repo_id': repo_model.id, 'plugin_id': named_row.id} + self._set_model_item(path, 'installed', True) + self._set_model_item(path, 'version', self.catalog_plugins.get_collection(catalog_model.id, repo_model.id)[named_row.id]['version']) + self.logger.info("installed plugin {} from catalog:{}, repository:{}".format(named_row.id, catalog_model.id, repo_model.id)) self.application.plugin_manager.load_all(on_error=self._on_plugin_load_error) def _disable_plugin(self, path, is_path=True): @@ -484,42 +477,52 @@ def _disable_plugin(self, path, is_path=True): else: path[self._named_model._fields.index('enabled')] = False - def _remove_matching_plugin(self, path, installed_plugin_info): + def _remove_matching_plugin(self, path, plugin_src): named_row = self._named_model(*self._model[path]) + repo_model = None for catalog_model in self._model: - if self._named_model(*catalog_model).id != installed_plugin_info['catalog_id']: + catalog_id = self._named_model(*catalog_model).id + if plugin_src and catalog_id == plugin_src['catalog_id']: + repo_model = next((rm for rm in catalog_model.iterchildren() if self._named_model(*rm).id == plugin_src['repo_id']), None) + break + elif plugin_src is None and catalog_id == _LOCAL_REPOSITORY_ID: + # local installation acts as a pseudo-repository + repo_model = catalog_model + break + if not repo_model: + return False + for plugin_model in repo_model.iterchildren(): + named_model = self._named_model(*plugin_model) + if named_model.id != named_row.id: continue - for repo_model in catalog_model.iterchildren(): - if self._named_model(*repo_model).id != installed_plugin_info['repo_id']: - continue - for plugin_model in repo_model.iterchildren(): - named_model = self._named_model(*plugin_model) - if named_model.id != named_row.id: - continue - if named_model.enabled: - self._disable_plugin(plugin_model, is_path=False) - self._uninstall_plugin( - self.catalog_plugins.get_collection(installed_plugin_info['catalog_id'], installed_plugin_info['repo_id']), - plugin_model, - is_path=False - ) - return True + if named_model.enabled: + self._disable_plugin(plugin_model, is_path=False) + self._uninstall_plugin(plugin_model.path) + return True + return False def _get_plugin_model_parents(self, plugin_model): return self._named_model(*plugin_model.parent), self._named_model(*plugin_model.parent.parent) - def _uninstall_plugin(self, plugin_collection, path, is_path=True): - plugin_id = self._named_model(*(self._model[path] if is_path else path)).id - for files in plugin_collection[plugin_id]['files']: - file_name = files[0] - if os.path.isfile(os.path.join(self.plugin_path, file_name)): - os.remove(os.path.join(self.plugin_path, file_name)) - self.application.plugin_manager.unload(plugin_id) + def _uninstall_plugin(self, model_path): + model_row = self._model[model_path] + plugin_id = self._named_model(*model_row).id + if os.path.isfile(os.path.join(self.plugin_path, plugin_id, '__init__.py')): + shutil.rmtree(os.path.join(self.plugin_path, plugin_id)) + elif os.path.isfile(os.path.join(self.plugin_path, plugin_id + '.py')): + os.remove(os.path.join(self.plugin_path, plugin_id + '.py')) + else: + self.logger.warning("failed to find plugin {0} on disk for removal".format(plugin_id)) + return False + self.application.plugin_manager.unload(plugin_id) del self.config['plugins.installed'][plugin_id] - if is_path: - self._set_model_item(path, 'installed', False) + + if model_row.parent and model_row.parent[self._named_model._fields.index('id')] == _LOCAL_REPOSITORY_ID: + del self._model[model_path] else: - path[self._named_model._fields.index('installed')] = False + self._set_model_item(model_path, 'installed', False) + self.logger.info("successfully uninstalled plugin {0}".format(plugin_id)) + return True def _set_info(self, model_instance): named_model = self._named_model(*model_instance) From 92d8c06e81dc8fcd5b8868fae10add86bfb6b854 Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Wed, 8 Nov 2017 15:16:22 -0500 Subject: [PATCH 22/38] Fixed import error in requirment check --- king_phisher/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/king_phisher/plugins.py b/king_phisher/plugins.py index 9ffac38c..ec58260c 100644 --- a/king_phisher/plugins.py +++ b/king_phisher/plugins.py @@ -170,7 +170,10 @@ def _check_for_missing_packages(self, packages): missing_packages = [] for package in packages: if package.startswith('gi.'): - if not importlib.util.find_spec(package): + try: + if not importlib.util.find_spec(package): + missing_packages.append(package) + except ImportError: missing_packages.append(package) continue package_check = smoke_zephyr.requirements.check_requirements([package]) From fc8c87d488330c9f26567d52c06091cd56657684 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 9 Nov 2017 10:27:39 -0500 Subject: [PATCH 23/38] Restore py2.7 compatibility for now --- king_phisher/client/application.py | 8 ++++---- king_phisher/client/windows/plugin_manager.py | 7 +++---- king_phisher/plugins.py | 1 + king_phisher/utilities.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/king_phisher/client/application.py b/king_phisher/client/application.py index 8334f4aa..4be81d9a 100644 --- a/king_phisher/client/application.py +++ b/king_phisher/client/application.py @@ -83,13 +83,12 @@ """The default GTK3 Theme for style information.""" USER_DATA_PATH = 'king-phisher' -"""The default folder location of user specific data storage.""" +"""The default folder name for user specific data storage.""" if its.mocked: _Gtk_Application = type('Gtk.Application', (object,), {'__module__': ''}) else: _Gtk_Application = Gtk.Application - USER_DATA_PATH = os.path.join(GLib.get_user_config_dir(), USER_DATA_PATH) class KingPhisherClientApplication(_Gtk_Application): """ @@ -131,6 +130,7 @@ def __init__(self, config_file=None, use_plugins=True, use_style=True): self._theme_file = 'theme.v1.css' else: self._theme_file = DISABLED + self.user_data_path = os.path.join(GLib.get_user_config_dir(), USER_DATA_PATH) self.logger = logging.getLogger('KingPhisher.Client.Application') # log version information for debugging purposes self.logger.debug("gi.repository GLib version: {0}".format('.'.join(map(str, GLib.glib_version)))) @@ -145,7 +145,7 @@ def __init__(self, config_file=None, use_plugins=True, use_style=True): self.set_flags(Gio.ApplicationFlags.NON_UNIQUE) self.set_property('application-id', 'org.king-phisher.client') self.set_property('register-session', True) - self.config_file = config_file or os.path.join(USER_DATA_PATH, 'config.json') + self.config_file = config_file or os.path.join(self.user_data_path, 'config.json') """The file containing the King Phisher client configuration.""" if not os.path.isfile(self.config_file): self._create_config() @@ -179,7 +179,7 @@ def __init__(self, config_file=None, use_plugins=True, use_style=True): self.logger.info('disabling all plugins') self.config['plugins.enabled'] = [] self.plugin_manager = plugins.ClientPluginManager( - [os.path.join(USER_DATA_PATH, 'plugins'), find.data_directory('plugins')], + [os.path.join(self.user_data_path, 'plugins'), find.data_directory('plugins')], self ) if use_plugins: diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 6127808e..326e60f7 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -37,10 +37,9 @@ import traceback from king_phisher import utilities -from king_phisher.client import application +from king_phisher.client import plugins from king_phisher.client import gui_utilities from king_phisher.client.widget import managers -from king_phisher.client.plugins import ClientCatalogManager from gi.repository import Gdk from gi.repository import Gtk @@ -131,7 +130,7 @@ def __init__(self, *args, **kwargs): self._model = Gtk.TreeStore(str, bool, bool, str, str, str, bool, bool, bool, str) self._model.set_sort_column_id(3, Gtk.SortType.ASCENDING) treeview.set_model(self._model) - self.plugin_path = os.path.join(application.USER_DATA_PATH, 'plugins') + self.plugin_path = os.path.join(self.application.user_data_path, 'plugins') self.load_thread = utilities.Thread(target=self._load_catalogs) self.load_thread.start() self.popup_menu = tvm.get_popup_menu() @@ -160,7 +159,7 @@ def signal_window_show(self, _): def _load_catalogs(self): self._update_status_bar('Loading, downloading catalogs...', idle=True) - self.catalog_plugins = ClientCatalogManager(application.USER_DATA_PATH) + self.catalog_plugins = plugins.ClientCatalogManager(self.application.user_data_path) for catalog in self.config['catalogs']: self.logger.debug("downloading catalog: {}".format(catalog)) self._update_status_bar("Loading, downloading catalog: {}".format(catalog)) diff --git a/king_phisher/plugins.py b/king_phisher/plugins.py index ec58260c..293e3106 100644 --- a/king_phisher/plugins.py +++ b/king_phisher/plugins.py @@ -388,6 +388,7 @@ def enable(self, name): self._lock.acquire() klass = self.loaded_plugins[name] if not klass.is_compatible: + self._lock.release() raise errors.KingPhisherPluginError(name, 'the plugin is incompatible') inst = klass(*self.plugin_init_args) try: diff --git a/king_phisher/utilities.py b/king_phisher/utilities.py index fa014843..96730f8c 100644 --- a/king_phisher/utilities.py +++ b/king_phisher/utilities.py @@ -513,7 +513,7 @@ def is_stopped(self): """ return self.stop_flag.is_set() -class Event(threading.Event): +class Event(getattr(threading, ('_Event' if hasattr(threading, '_Event') else 'Event'))): __slots__ = ('__event',) def __init__(self): super(Event, self).__init__() From a90ff09ee7614f1c64da8a364a9a8a19bca4456d Mon Sep 17 00:00:00 2001 From: wolfthefallen Date: Mon, 13 Nov 2017 16:12:41 -0500 Subject: [PATCH 24/38] Pinned graphene to 1.4.1 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2f082458..caadfab2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,8 @@ ecdsa>=0.13 email-validator>=1.0.2 geoip2>=2.5.0 geojson>=1.3.5 -graphene>=1.4 -graphene-sqlalchemy>=1.1.1 +graphene==1.4.1 +graphene-sqlalchemy==1.1.1 graphql-relay>=0.4.5 icalendar>=3.11.4 ipaddress>=1.0.18 ; python_version < '3.3' From 321fd9532763c3698a9ab503c04ab75300ca0bf2 Mon Sep 17 00:00:00 2001 From: Erik Daguerre Date: Tue, 14 Nov 2017 09:06:25 -0500 Subject: [PATCH 25/38] Pinned graphql to 1.1 --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index caadfab2..1709cc34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,8 @@ geoip2>=2.5.0 geojson>=1.3.5 graphene==1.4.1 graphene-sqlalchemy==1.1.1 -graphql-relay>=0.4.5 +graphql-core==1.1 +graphql-relay==0.4.5 icalendar>=3.11.4 ipaddress>=1.0.18 ; python_version < '3.3' jsonschema>=2.6.0 From c14c5e00d0c23db98172c1cae8ba4c7c56996583 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 15 Nov 2017 15:56:38 -0500 Subject: [PATCH 26/38] Update GraphQL code to work with v2 --- docs/requirements.rtd.txt | 6 +- king_phisher/server/graphql.py | 184 +++++++++++++++++---------------- requirements.txt | 5 +- 3 files changed, 102 insertions(+), 93 deletions(-) diff --git a/docs/requirements.rtd.txt b/docs/requirements.rtd.txt index 1b7b5a36..d36cc3c5 100644 --- a/docs/requirements.rtd.txt +++ b/docs/requirements.rtd.txt @@ -10,9 +10,9 @@ ecdsa>=0.13 email-validator>=1.0.2 geoip2>=2.5.0 geojson>=1.3.5 -graphene>=1.4 -graphene-sqlalchemy>=1.1.1 -graphql-relay>=0.4.5 +graphene==2.0.1 +graphene-sqlalchemy==2.0.0 +graphql-relay==0.4.5 icalendar>=3.11.4 ipaddress>=1.0.18 ; python_version < '3.3' jsonschema>=2.6.0 diff --git a/king_phisher/server/graphql.py b/king_phisher/server/graphql.py index d2fa1ebb..009947bf 100644 --- a/king_phisher/server/graphql.py +++ b/king_phisher/server/graphql.py @@ -41,6 +41,7 @@ import graphene import graphene.relay.connection import graphene.types +import graphene.types.utils import graphql_relay.connection.arrayconnection import graphene_sqlalchemy @@ -67,17 +68,17 @@ def connection_factory(cls, node): return connection_type @classmethod - def connection_resolver(cls, resolver, connection, model, root, args, context, info): - iterable = resolver(root, args, context, info) + def connection_resolver(cls, resolver, connection, model, root, info, **kwargs): + iterable = resolver(root, info, **kwargs) if iterable is None: - iterable = cls.get_query(model, context, info, args) + iterable = cls.get_query(model, info, **kwargs) if isinstance(iterable, Query): _len = iterable.count() else: _len = len(iterable) - return graphql_relay.connection.arrayconnection.connection_from_list_slice( + connection = graphql_relay.connection.arrayconnection.connection_from_list_slice( iterable, - args, + kwargs, slice_start=0, list_length=_len, list_slice_length=_len, @@ -85,6 +86,15 @@ def connection_resolver(cls, resolver, connection, model, root, args, context, i pageinfo_type=graphene.relay.connection.PageInfo, edge_type=connection.Edge ) + connection.iterable = iterable + connection.length = _len + return connection + + @property + def type(self): + # this is to bypass the one from + # graphene_sqlalchemy.SQLAlchemyConnectionField which breaks + return graphene.types.utils.get_type(self._type) # replacement graphql types class DateTime(graphene.types.Scalar): @@ -152,8 +162,8 @@ class PluginConnection(graphene.relay.Connection): class Meta: node = Plugin total = graphene.Int() - def resolve_total(self, args, context, info): - return len(context.get('plugin_manager', {})) + def resolve_total(self, info, **kwargs): + return len(info.context.get('plugin_manager', {})) # database graphql objects class AlertSubscription(graphene_sqlalchemy.SQLAlchemyObjectType): @@ -175,7 +185,7 @@ class Meta: first_visit = DateTime() last_visit = DateTime() visitor_geoloc = graphene.Field(GeoLocation) - def resolve_visitor_geoloc(self, args, context, info): + def resolve_visitor_geoloc(self, info, **kwargs): visitor_ip = self.visitor_ip if not visitor_ip: return @@ -202,7 +212,7 @@ class Meta: visitor_geoloc = graphene.Field(GeoLocation) # relationships credentials = SQLAlchemyConnectionField(Credential) - def resolve_visitor_geoloc(self, args, context, info): + def resolve_visitor_geoloc(self, info, **kwargs): visitor_ip = self.visitor_ip if not visitor_ip: return @@ -218,7 +228,7 @@ class Meta: # relationships credentials = SQLAlchemyConnectionField(Credential) visits = SQLAlchemyConnectionField(Visit) - def resolve_opener_geoloc(self, args, context, info): + def resolve_opener_geoloc(self, info, **kwargs): opener_ip = self.opener_ip if not opener_ip: return @@ -302,121 +312,121 @@ class Database(graphene.ObjectType): users = SQLAlchemyConnectionField(User) visit = graphene.Field(Visit, id=graphene.String()) visits = SQLAlchemyConnectionField(Visit) - def resolve_alert_subscription(self, args, context, info): - query = AlertSubscription.get_query(context) - query = query.filter_by(**args) + def resolve_alert_subscription(self, info, **kwargs): + query = AlertSubscription.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_alert_subscriptions(self, args, context, info): - query = AlertSubscription.get_query(context) + def resolve_alert_subscriptions(self, info, **kwargs): + query = AlertSubscription.get_query(info) return query.all() - def resolve_campaign(self, args, context, info): - query = Campaign.get_query(context) - query = query.filter_by(**args) + def resolve_campaign(self, info, **kwargs): + query = Campaign.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_campaigns(self, args, context, info): - query = Campaign.get_query(context) + def resolve_campaigns(self, info, **kwargs): + query = Campaign.get_query(info) return query.all() - def resolve_campaign_type(self, args, context, info): - query = CampaignType.get_query(context) - query = query.filter_by(**args) + def resolve_campaign_type(self, info, **kwargs): + query = CampaignType.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_campaign_types(self, args, context, info): - query = CampaignType.get_query(context) + def resolve_campaign_types(self, info, **kwargs): + query = CampaignType.get_query(info) return query.all() - def resolve_company(self, args, context, info): - query = Company.get_query(context) - query = query.filter_by(**args) + def resolve_company(self, info, **kwargs): + query = Company.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_companies(self, args, context, info): - query = Company.get_query(context) + def resolve_companies(self, info, **kwargs): + query = Company.get_query(info) return query.all() - def resolve_company_department(self, args, context, info): - query = CompanyDepartment.get_query(context) - query = query.filter_by(**args) + def resolve_company_department(self, info, **kwargs): + query = CompanyDepartment.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_company_departments(self, args, context, info): - query = CompanyDepartment.get_query(context) + def resolve_company_departments(self, info, **kwargs): + query = CompanyDepartment.get_query(info) return query.all() - def resolve_credential(self, args, context, info): - query = Credential.get_query(context) - query = query.filter_by(**args) + def resolve_credential(self, info, **kwargs): + query = Credential.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_credentials(self, args, context, info): - query = Credential.get_query(context) + def resolve_credentials(self, info, **kwargs): + query = Credential.get_query(info) return query.all() - def resolve_deaddrop_connection(self, args, context, info): - query = DeaddropConnection.get_query(context) - query = query.filter_by(**args) + def resolve_deaddrop_connection(self, info, **kwargs): + query = DeaddropConnection.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_deaddrop_connections(self, args, context, info): - query = DeaddropConnection.get_query(context) + def resolve_deaddrop_connections(self, info, **kwargs): + query = DeaddropConnection.get_query(info) return query.all() - def resolve_deaddrop_deployment(self, args, context, info): - query = DeaddropDeployment.get_query(context) - query = query.filter_by(**args) + def resolve_deaddrop_deployment(self, info, **kwargs): + query = DeaddropDeployment.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_deaddrop_deployments(self, args, context, info): - query = DeaddropDeployment.get_query(context) + def resolve_deaddrop_deployments(self, info, **kwargs): + query = DeaddropDeployment.get_query(info) return query.all() - def resolve_industry(self, args, context, info): - query = Industry.get_query(context) - query = query.filter_by(**args) + def resolve_industry(self, info, **kwargs): + query = Industry.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_industries(self, args, context, info): - query = Industry.get_query(context) + def resolve_industries(self, info, **kwargs): + query = Industry.get_query(info) return query.all() - def resolve_landing_page(self, args, context, info): - query = LandingPage.get_query(context) - query = query.filter_by(**args) + def resolve_landing_page(self, info, **kwargs): + query = LandingPage.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_landing_pages(self, args, context, info): - query = LandingPage.get_query(context) + def resolve_landing_pages(self, info, **kwargs): + query = LandingPage.get_query(info) return query.all() - def resolve_message(self, args, context, info): - query = Message.get_query(context) - query = query.filter_by(**args) + def resolve_message(self, info, **kwargs): + query = Message.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_messages(self, args, context, info): - query = Message.get_query(context) + def resolve_messages(self, info, **kwargs): + query = Message.get_query(info) return query.all() - def resolve_user(self, args, context, info): - query = User.get_query(context) - query = query.filter_by(**args) + def resolve_user(self, info, **kwargs): + query = User.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_users(self, args, context, info): - query = User.get_query(context) + def resolve_users(self, info, **kwargs): + query = User.get_query(info) return query.all() - def resolve_visit(self, args, context, info): - query = Visit.get_query(context) - query = query.filter_by(**args) + def resolve_visit(self, info, **kwargs): + query = Visit.get_query(info) + query = query.filter_by(**kwargs) return query.first() - def resolve_visits(self, args, context, info): - query = Visit.get_query(context) + def resolve_visits(self, info, **kwargs): + query = Visit.get_query(info) return query.all() # top level query object for the schema @@ -429,27 +439,27 @@ class Query(graphene.ObjectType): plugin = graphene.Field(Plugin, name=graphene.String()) plugins = graphene.relay.ConnectionField(PluginConnection) version = graphene.Field(graphene.String) - def resolve_db(self, args, context, info): + def resolve_db(self, info, **kwargs): return Database() - def resolve_geoloc(self, args, context, info): - ip_address = args.get('ip') + def resolve_geoloc(self, info, **kwargs): + ip_address = kwargs.get('ip') if ip_address is None: return return GeoLocation.from_ip_address(ip_address) - def resolve_plugin(self, args, context, info): - plugin_manager = context.get('plugin_manager', {}) + def resolve_plugin(self, info, **kwargs): + plugin_manager = info.context.get('plugin_manager', {}) for _, plugin in plugin_manager: - if plugin.name != args.get('name'): + if plugin.name != kwargs.get('name'): continue return Plugin.from_plugin(plugin) - def resolve_plugins(self, args, context, info): - plugin_manager = context.get('plugin_manager', {}) + def resolve_plugins(self, info, **kwargs): + plugin_manager = info.context.get('plugin_manager', {}) return [Plugin.from_plugin(plugin) for _, plugin in sorted(plugin_manager, key=lambda i: i[0])] - def resolve_version(self, args, context, info): + def resolve_version(self, info, **kwargs): return version.version class AuthorizationMiddleware(object): @@ -460,12 +470,12 @@ class AuthorizationMiddleware(object): and all objects and operations will be accessible. The **rpc_session** key's value must be an instance of :py:class:`~.AuthenticatedSession`. """ - def resolve(self, next_, root, args, context, info): - rpc_session = context.get('rpc_session') + def resolve(self, next_, root, info, **kwargs): + rpc_session = info.context.get('rpc_session') if isinstance(root, db_models.Base) and rpc_session is not None: if not root.session_has_read_prop_access(rpc_session, info.field_name): return - return next_(root, args, context, info) + return next_(root, info, **kwargs) class Schema(graphene.Schema): """ diff --git a/requirements.txt b/requirements.txt index 1709cc34..869dbc23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,8 @@ ecdsa>=0.13 email-validator>=1.0.2 geoip2>=2.5.0 geojson>=1.3.5 -graphene==1.4.1 -graphene-sqlalchemy==1.1.1 -graphql-core==1.1 +graphene==2.0.1 +graphene-sqlalchemy==2.0.0 graphql-relay==0.4.5 icalendar>=3.11.4 ipaddress>=1.0.18 ; python_version < '3.3' From 8ce90c34e1343b96c6eb1b5e130e2b36261218e6 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 Nov 2017 09:36:04 -0500 Subject: [PATCH 27/38] Fix login error dialog stacking --- king_phisher/client/application.py | 48 +++++++++++++++++------------ king_phisher/client/windows/main.py | 8 ++--- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/king_phisher/client/application.py b/king_phisher/client/application.py index 4be81d9a..98abf158 100644 --- a/king_phisher/client/application.py +++ b/king_phisher/client/application.py @@ -201,7 +201,7 @@ def _create_actions(self): self.actions['emit-application-signal'] = action self.add_action(action) - def _create_ssh_forwarder(self, server, username, password): + def _create_ssh_forwarder(self, server, username, password, window=None): """ Create and set the :py:attr:`~.KingPhisherClientApplication._ssh_forwarder` attribute. @@ -209,10 +209,12 @@ def _create_ssh_forwarder(self, server, username, password): :param tuple server: The server information as a host and port tuple. :param str username: The username to authenticate to the SSH server with. :param str password: The password to authenticate to the SSH server with. + :param window: The GTK window to use as the parent for error dialogs. + :type window: :py:class:`Gtk.Window` :rtype: int :return: The local port that is forwarded to the remote server or None if the connection failed. """ - active_window = self.get_active_window() + window = window or self.get_active_window() title_ssh_error = 'Failed To Connect To The SSH Service' server_remote_port = self.config['server_remote_port'] @@ -227,25 +229,21 @@ def _create_ssh_forwarder(self, server, username, password): ) self._ssh_forwarder.start() except ssh_forward.KingPhisherSSHKeyError as error: - gui_utilities.show_dialog_error( - 'SSH Key Configuration Error', - active_window, - error.message - ) + gui_utilities.show_dialog_error('SSH Key Configuration Error', window, error.message) except errors.KingPhisherAbortError as error: self.logger.info("ssh connection aborted ({0})".format(error.message)) except paramiko.PasswordRequiredException: - gui_utilities.show_dialog_error(title_ssh_error, active_window, 'The specified SSH key requires a password.') + gui_utilities.show_dialog_error(title_ssh_error, window, 'The specified SSH key requires a password.') except paramiko.AuthenticationException: self.logger.warning('failed to authenticate to the remote ssh server') - gui_utilities.show_dialog_error(title_ssh_error, active_window, 'The server responded that the credentials are invalid.') + gui_utilities.show_dialog_error(title_ssh_error, window, 'The server responded that the credentials are invalid.') except paramiko.SSHException as error: self.logger.warning("failed with ssh exception '{0}'".format(error.args[0])) except socket.error as error: - gui_utilities.show_dialog_exc_socket_error(error, active_window, title=title_ssh_error) + gui_utilities.show_dialog_exc_socket_error(error, window, title=title_ssh_error) except Exception as error: self.logger.warning('failed to connect to the remote ssh server', exc_info=True) - gui_utilities.show_dialog_error(title_ssh_error, active_window, "An {0}.{1} error occurred.".format(error.__class__.__module__, error.__class__.__name__)) + gui_utilities.show_dialog_error(title_ssh_error, window, "An {0}.{1} error occurred.".format(error.__class__.__module__, error.__class__.__name__)) else: return self._ssh_forwarder.local_server self.emit('server-disconnected') @@ -536,18 +534,28 @@ def load_style_css(self, css_file): ) return style_provider - def server_connect(self, username, password, otp=None): + def server_connect(self, username, password, otp=None, window=None): + """ + Initialize the connection to the King Phisher server. + + :param str username: The username to authenticate with. + :param str password: The password to authenticate with. + :param str otp: The optional one-time password to authenticate with. + :param window: The GTK window to use as the parent for error dialogs. + :type window: :py:class:`Gtk.Window` + :rtype: tuple + """ # pylint: disable=too-many-locals server_version_info = None title_rpc_error = 'Failed To Connect To The King Phisher RPC Service' - active_window = self.get_active_window() + window = window or self.get_active_window() server = parse_server(self.config['server'], 22) if ipaddress.is_loopback(server[0]): local_server = ('localhost', self.config['server_remote_port']) self.logger.info("connecting to local king phisher instance") else: - local_server = self._create_ssh_forwarder(server, username, password) + local_server = self._create_ssh_forwarder(server, username, password, window=window) if not local_server: return False, ConnectionErrorReason.ERROR_PORT_FORWARD @@ -567,19 +575,19 @@ def server_connect(self, username, password, otp=None): raise RuntimeError('no version information was retrieved from the server') except advancedhttpserver.RPCError as error: self.logger.warning('failed to connect to the remote rpc service due to http status: ' + str(error.status)) - gui_utilities.show_dialog_error(title_rpc_error, active_window, "The server responded with HTTP status: {0}.".format(str(error.status))) + gui_utilities.show_dialog_error(title_rpc_error, window, "The server responded with HTTP status: {0}.".format(str(error.status))) except BadStatusLine as error: self.logger.warning('failed to connect to the remote rpc service due to http bad status line: ' + error.line) - gui_utilities.show_dialog_error(title_rpc_error, active_window, generic_message) + gui_utilities.show_dialog_error(title_rpc_error, window, generic_message) except socket.error as error: self.logger.debug('failed to connect to the remote rpc service due to a socket error', exc_info=True) - gui_utilities.show_dialog_exc_socket_error(error, active_window) + gui_utilities.show_dialog_exc_socket_error(error, window) except ssl.CertificateError as error: self.logger.warning('failed to connect to the remote rpc service with a https certificate error: ' + error.message) - gui_utilities.show_dialog_error(title_rpc_error, active_window, 'The server presented an invalid SSL certificate.') + gui_utilities.show_dialog_error(title_rpc_error, window, 'The server presented an invalid SSL certificate.') except Exception: self.logger.warning('failed to connect to the remote rpc service', exc_info=True) - gui_utilities.show_dialog_error(title_rpc_error, active_window, generic_message) + gui_utilities.show_dialog_error(title_rpc_error, window, generic_message) else: connection_failed = False @@ -607,7 +615,7 @@ def server_connect(self, username, password, otp=None): error_text = 'The client is running an old and incompatible version.' error_text += '\nPlease update the local client installation.' if error_text: - gui_utilities.show_dialog_error('The RPC API Versions Are Incompatible', active_window, error_text) + gui_utilities.show_dialog_error('The RPC API Versions Are Incompatible', window, error_text) self.emit('server-disconnected') return False, ConnectionErrorReason.ERROR_INCOMPATIBLE_VERSIONS diff --git a/king_phisher/client/windows/main.py b/king_phisher/client/windows/main.py index cc81df7b..6d73cacd 100755 --- a/king_phisher/client/windows/main.py +++ b/king_phisher/client/windows/main.py @@ -310,19 +310,19 @@ def signal_login_dialog_response(self, dialog, response): otp = self.config['server_one_time_password'] if not otp: otp = None - _, reason = self.application.server_connect(username, password, otp) + _, reason = self.application.server_connect(username, password, otp, window=dialog) if reason == ConnectionErrorReason.ERROR_INVALID_OTP: revealer = self.login_dialog.gobjects['revealer_server_one_time_password'] if revealer.get_child_revealed(): - gui_utilities.show_dialog_error('Login Failed', self, 'A valid one time password (OTP) token is required.') + gui_utilities.show_dialog_error('Login Failed', dialog, 'A valid one time password (OTP) token is required.') else: revealer.set_reveal_child(True) entry = self.login_dialog.gobjects['entry_server_one_time_password'] entry.grab_focus() elif reason == ConnectionErrorReason.ERROR_INVALID_CREDENTIALS: - gui_utilities.show_dialog_error('Login Failed', self, 'The provided credentials are incorrect.') + gui_utilities.show_dialog_error('Login Failed', dialog, 'The provided credentials are incorrect.') elif reason == ConnectionErrorReason.ERROR_UNKNOWN: - gui_utilities.show_dialog_error('Login Failed', self, 'An unknown error has occurred.') + gui_utilities.show_dialog_error('Login Failed', dialog, 'An unknown error has occurred.') def export_campaign_xlsx(self): """Export the current campaign to an Excel compatible XLSX workbook.""" From d94756a1a1b9ff2a4d307b04d0084cf1939d1f93 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 Nov 2017 11:37:58 -0500 Subject: [PATCH 28/38] Add additional graphql query examples --- docs/source/server/graphql.rst | 42 ++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/source/server/graphql.rst b/docs/source/server/graphql.rst index 79f9b500..013fd986 100644 --- a/docs/source/server/graphql.rst +++ b/docs/source/server/graphql.rst @@ -42,8 +42,8 @@ string parameter and optional query variables. This can be used for easily testing queries. It should be noted however that using this utility directly on the server does not restrict access to data as the RPC interface does. -Example Query -------------- +Example Queries +--------------- The following query is an example of retrieving the first 3 users from the users table. The query includes the necessary information to perform subsequent @@ -79,5 +79,43 @@ queries to iterate over all entries. } } +.. code-block:: none + + # Get a summary of all of the campaigns + query getCampaigns { + db { + campaigns { + # get the total number of campaigns + total + edges { + node { + id + created + name + # get the details about the user that created this campaign + user { + id + phoneNumber + } + # get the total number of messages in this campaign + messages { + total + } + # get the total number of visits in this campaign + visits { + total + } + } + } + } + } + } + +.. code-block:: none + + # This query does not define the operation type or an operation name + # and is condensed to a single line + { plugins { total edges { node { name title authors } } } } + .. _GraphQL: http://graphql.org/ .. _Relay: https://facebook.github.io/relay/graphql/connections.htm From 70566f98bc6cac439d23f7f7d7a94a6ed50b2abd Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 20 Nov 2017 10:38:45 -0500 Subject: [PATCH 29/38] Fix the color of exported graphs --- king_phisher/client/graphs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/king_phisher/client/graphs.py b/king_phisher/client/graphs.py index 7f59ac27..72d55e79 100644 --- a/king_phisher/client/graphs.py +++ b/king_phisher/client/graphs.py @@ -276,7 +276,7 @@ def signal_activate_popup_menu_export(self, action): if not response: return destination_file = response['target_path'] - self.figure.savefig(destination_file, format='png') + self.figure.savefig(destination_file, dpi=200, facecolor=self.figure.get_facecolor(), format='png') def signal_activate_popup_refresh(self, event): self.refresh() From 87d5b4959013e7dcf945401432971a60a3be7730 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 20 Nov 2017 16:05:59 -0500 Subject: [PATCH 30/38] Remove a piece of unnecessary code --- king_phisher/client/windows/plugin_manager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index 326e60f7..e302b735 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -421,11 +421,7 @@ def signal_renderer_toggled_enable(self, _, path): return if named_row.id not in pm.loaded_plugins: return - if self._named_model(*self._model[path].parent).id: - plugin_src = self.config['plugins.installed'][named_row.id] - repo_model, catalog_model = self._get_plugin_model_parents(self._model[path]) - if plugin_src and (repo_model.id != plugin_src['repo_id'] or catalog_model.id != plugin_src['catalog_id']): - return + if named_row.id in self._module_errors: gui_utilities.show_dialog_error('Can Not Enable Plugin', self.window, 'Can not enable a plugin which failed to load.') return From c046173d4b1aa57662fcb0d5974c44bd1ee669bb Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 21 Nov 2017 13:20:28 -0500 Subject: [PATCH 31/38] Store an id attribute with ecdsa keys --- king_phisher/catalog.py | 5 ++- king_phisher/client/plugins.py | 1 - king_phisher/security_keys.py | 56 ++++++++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/king_phisher/catalog.py b/king_phisher/catalog.py index d994b859..4c4ab926 100644 --- a/king_phisher/catalog.py +++ b/king_phisher/catalog.py @@ -392,7 +392,7 @@ def add_catalog_url(self, url): return return catalog -def sign_item_files(local_path, signing_key, signing_key_id, repo_path=None): +def sign_item_files(local_path, signing_key, repo_path=None): """ This utility function is used to create a :py:class:`.CollectionItemFile` iterator from the specified source to be included in either a catalog file @@ -400,7 +400,6 @@ def sign_item_files(local_path, signing_key, signing_key_id, repo_path=None): :param str local_path: The real location of where the files exist on disk. :param signing_key: The key with which to sign the files for verification. - :param str signing_key_id: The unique identifier for *signing_key*. :param str repo_path: The path of the repository as it exists on disk. """ local_path = os.path.abspath(local_path) @@ -429,6 +428,6 @@ def sign_item_files(local_path, signing_key, signing_key_id, repo_path=None): path_destination=destination_file_path, path_source=source_file_path, signature=signature, - signed_by=signing_key_id + signed_by=signing_key.id ) yield item_file diff --git a/king_phisher/client/plugins.py b/king_phisher/client/plugins.py index c0a9c865..19642be2 100644 --- a/king_phisher/client/plugins.py +++ b/king_phisher/client/plugins.py @@ -632,7 +632,6 @@ def get_collection(self, catalog_id, repo_id): :param str catalog_id: The name of the catalog the repo belongs to :param repo_id: The id of the repository requested. :return: The the collection of manager type from the specified catalog and repository. - :rtype:py:class: """ return self.catalogs[catalog_id].repositories[repo_id].collections.get(self.manager_type) diff --git a/king_phisher/security_keys.py b/king_phisher/security_keys.py index 1948ccf5..0f285b42 100644 --- a/king_phisher/security_keys.py +++ b/king_phisher/security_keys.py @@ -79,9 +79,9 @@ def _encoding_data(value, encoding=None): raise ValueError('unknown encoding: ' + encoding) return value -def _key_cls_from_dict(cls, value, encoding=None): +def _key_cls_from_dict(cls, value, encoding=None, **kwargs): key_data = _decode_data(value['data'], encoding=encoding) - return cls.from_string(key_data, curve=value['type']) + return cls.from_string(key_data, curve=value['type'], **kwargs) def _kwarg_curve(kwargs): if 'curve' not in kwargs: @@ -164,29 +164,41 @@ def openssl_derive_key_and_iv(password, salt, key_length, iv_length, digest='sha return data[:key_length], data[key_length:key_length + iv_length] class SigningKey(ecdsa.SigningKey, object): + def __init__(self, *args, **kwargs): + self.id = kwargs.pop('id', None) + """An optional string identifier for this key instance.""" + super(SigningKey, self).__init__(*args, **kwargs) + @classmethod def from_secret_exponent(cls, *args, **kwargs): + id_ = kwargs.pop('id', None) instance = super(SigningKey, cls).from_secret_exponent(*args, **kwargs) orig_vk = instance.verifying_key - instance.verifying_key = VerifyingKey.from_public_point(orig_vk.pubkey.point, instance.curve, instance.default_hashfunc) + instance.verifying_key = VerifyingKey.from_public_point(orig_vk.pubkey.point, instance.curve, instance.default_hashfunc, id=id_) + instance.id = id_ return instance @classmethod def from_string(cls, string, **kwargs): kwargs = _kwarg_curve(kwargs) - return super(SigningKey, cls).from_string(string, **kwargs) + id_ = kwargs.pop('id', None) + inst = super(SigningKey, cls).from_string(string, **kwargs) + inst.id = id_ + inst.verifying_key.id = id_ + return inst @classmethod - def from_dict(cls, value, encoding='base64'): + def from_dict(cls, value, encoding='base64', **kwargs): """ Load the signing key from the specified dict object. :param dict value: The dictionary to load the key data from. :param str encoding: The encoding of the required 'data' key. + :param dict kwargs: Additional key word arguments to pass to the class on initialization. :return: The new signing key. :rtype: :py:class:`.SigningKey` """ - return _key_cls_from_dict(cls, value, encoding=encoding) + return _key_cls_from_dict(cls, value, encoding=encoding, **kwargs) @classmethod def from_file(cls, file_path, password=None, encoding='utf-8'): @@ -209,7 +221,7 @@ def from_file(cls, file_path, password=None, encoding='utf-8'): file_data = file_data.decode(encoding) file_data = serializers.JSON.loads(file_data) utilities.validate_json_schema(file_data, 'king-phisher.security.key') - return file_data['id'], cls.from_dict(file_data['signing-key'], encoding=file_data.pop('encoding', 'base64')) + return cls.from_dict(file_data['signing-key'], encoding=file_data.pop('encoding', 'base64'), id=file_data['id']) def sign_dict(self, data, signature_encoding='base64'): """ @@ -231,14 +243,38 @@ def sign_dict(self, data, signature_encoding='base64'): return data class VerifyingKey(ecdsa.VerifyingKey, object): + def __init__(self, *args, **kwargs): + self.id = kwargs.pop('id', None) + """An optional string identifier for this key instance.""" + super(VerifyingKey, self).__init__(*args, **kwargs) + + @classmethod + def from_public_point(cls, *args, **kwargs): + id_ = kwargs.pop('id', None) + inst = super(VerifyingKey, cls).from_public_point(*args, **kwargs) + inst.id = id_ + return inst + @classmethod def from_string(cls, string, **kwargs): kwargs = _kwarg_curve(kwargs) - return super(VerifyingKey, cls).from_string(string, **kwargs) + id_ = kwargs.pop('id', None) + inst = super(VerifyingKey, cls).from_string(string, **kwargs) + inst.id = id_ + return inst @classmethod - def from_dict(cls, value, encoding='base64'): - return _key_cls_from_dict(cls, value, encoding=encoding) + def from_dict(cls, value, encoding='base64', **kwargs): + """ + Load the verifying key from the specified dict object. + + :param dict value: The dictionary to load the key data from. + :param str encoding: The encoding of the required 'data' key. + :param dict kwargs: Additional key word arguments to pass to the class on initialization. + :return: The new verifying key. + :rtype: :py:class:`.VerifyingKey` + """ + return _key_cls_from_dict(cls, value, encoding=encoding, **kwargs) def verify_dict(self, data, signature_encoding='base64'): """ From 9bce0f48aa9885989cb368e26f1c03376e252043 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 21 Nov 2017 16:18:31 -0500 Subject: [PATCH 32/38] Add login to the possible words for username --- king_phisher/server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/king_phisher/server/server.py b/king_phisher/server/server.py index 60b9b946..e5f09ff8 100644 --- a/king_phisher/server/server.py +++ b/king_phisher/server/server.py @@ -222,7 +222,7 @@ def get_query_creds(self, check_query=True): username = None password = '' - for pname in ('username', 'user', 'u'): + for pname in ('username', 'user', 'u', 'login'): username = (self.get_query(pname) or self.get_query(pname.title()) or self.get_query(pname.upper())) if username: break From 5f801900a8858a885e9546ccb69a27c76e7066c5 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 21 Nov 2017 16:47:41 -0500 Subject: [PATCH 33/38] Update the documentation of security_keys --- king_phisher/security_keys.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/king_phisher/security_keys.py b/king_phisher/security_keys.py index 0f285b42..5fca05c8 100644 --- a/king_phisher/security_keys.py +++ b/king_phisher/security_keys.py @@ -143,7 +143,8 @@ def openssl_derive_key_and_iv(password, salt, key_length, iv_length, digest='sha Different versions of OpenSSL use a different default value for the *digest* function used to derive keys and initialization vectors. A specific one can be used by passing the ``-md`` option to the - ``openssl`` command. + ``openssl`` command which corresponds to the *digest* parameter of this + function. :param str password: The password to use when deriving the key and IV. :param bytes salt: A value to use as a salt for the operation. @@ -204,8 +205,10 @@ def from_dict(cls, value, encoding='base64', **kwargs): def from_file(cls, file_path, password=None, encoding='utf-8'): """ Load the signing key from the specified file. If *password* is - specified, the file is assumed to have been encoded using OpenSSL using - ``aes-256-cbc`` with ``sha256`` as the message digest. + specified, the file is assumed to have been encrypted using OpenSSL + with ``aes-256-cbc`` as the cipher and ``sha256`` as the message + digest. This uses :py:func:`.openssl_decrypt_data` internally for + decrypting the data. :param str file_path: The path to the file to load. :param str password: An optional password to use for decrypting the file. From f99b0dfa43b875113d568d09e8e2ec1f36577f12 Mon Sep 17 00:00:00 2001 From: Erik Daguerre Date: Tue, 21 Nov 2017 16:56:26 -0500 Subject: [PATCH 34/38] Updated cx_freeze.py --- tools/development/cx_freeze.py | 48 +++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/tools/development/cx_freeze.py b/tools/development/cx_freeze.py index d384954c..b9f87db3 100644 --- a/tools/development/cx_freeze.py +++ b/tools/development/cx_freeze.py @@ -56,7 +56,9 @@ 'lib\enchant\libenchant_myspell.dll', 'lib\enchant\libenchant_voikko.dll', 'lib\gio\modules\libgiognomeproxy.dll', + 'lib\gio\modules\libgiognutls.dll', 'lib\gio\modules\libgiolibproxy.dll', + 'lib\gio\modules\libgsettingsgconfbackend.dll', 'libaspell-15.dll', 'libatk-1.0-0.dll', 'libcairo-gobject-2.dll', @@ -67,29 +69,50 @@ 'libfontconfig-1.dll', 'libfreetype-6.dll', 'libgailutil-3-0.dll', - 'libgdk_pixbuf-2.0-0.dll', + 'libgconf-2-4.dll', 'libgdk-3-0.dll', + 'libgdk_pixbuf-2.0-0.dll', + 'libgee-0.8-2.dll', 'libgeoclue-0.dll', 'libgio-2.0-0.dll', 'libgirepository-1.0-1.dll', 'libglib-2.0-0.dll', 'libgmodule-2.0-0.dll', + 'libgnutls-26.dll', 'libgobject-2.0-0.dll', + 'libgstallocators-1.0-0.dll', 'libgstapp-1.0-0.dll', 'libgstaudio-1.0-0.dll', 'libgstbase-1.0-0.dll', + 'libgstcheck-1.0-0.dll', + 'libgstcontroller-1.0-0.dll', + 'libgstfft-1.0-0.dll', + 'libgstnet-1.0-0.dll', 'libgstpbutils-1.0-0.dll', 'libgstreamer-1.0-0.dll', + 'libgstriff-1.0-0.dll', + 'libgstrtp-1.0-0.dll', + 'libgstrtsp-1.0-0.dll', + 'libgstrtspserver-1.0-0.dll', + 'libgstsdp-1.0-0.dll', 'libgsttag-1.0-0.dll', + 'libgstvalidate-1.0-0.dll', + 'libgstvalidate-default-overrides-1.0-0.dll', 'libgstvideo-1.0-0.dll', + 'libgthread-2.0-0.dll', 'libgtk-3-0.dll', 'libgtksourceview-3.0-1.dll', + 'libgtkspell3-3-0.dll', + 'libgxml-0.4-4.dll', 'libharfbuzz-0.dll', + 'libharfbuzz-gobject-0.dll', 'libintl-8.dll', + 'libjack.dll', 'libjasper-1.dll', 'libjavascriptcoregtk-3.0-0.dll', 'libjpeg-8.dll', 'liborc-0.4-0.dll', + 'liborc-test-0.4-0.dll', 'libp11-kit-0.dll', 'libpango-1.0-0.dll', 'libpangocairo-1.0-0.dll', @@ -101,6 +124,7 @@ 'libsoup-2.4-1.dll', 'libsqlite3-0.dll', 'libtiff-5.dll', + 'libvisual-0.4-0.dll', 'libwebkitgtk-3.0-0.dll', 'libwebp-5.dll', 'libwinpthread-1.dll', @@ -117,6 +141,11 @@ for lib in gtk_libs: include_files.append((os.path.join(include_dll_path, lib), lib)) +# include all site-packages and eggs for pkg_resources to function correctly +for path in os.listdir(site.getsitepackages()[1]): + if os.path.isdir(os.path.join(site.getsitepackages()[1], path)): + include_files.append((os.path.join(site.getsitepackages()[1], path), path)) + # include windows complied version of geos for basemaps include_files.append((os.path.join(site.getsitepackages()[1], 'geos.dll'), 'geos.dll')) include_files.append((os.path.join(site.getsitepackages()[1], 'geos_c.dll'), 'geos_c.dll')) @@ -152,26 +181,39 @@ 'boltons', 'cairo', 'cffi', + 'collections', 'cryptography', 'dns', 'email', + 'email_validator', + 'geoip2', + 'geojson', 'gi', + 'graphene', + 'graphene_sqlalchemy', 'icalendar', 'idna', + 'ipaddress', 'jinja2', + 'jsonschema', 'king_phisher.client', 'matplotlib', 'mpl_toolkits', 'msgpack', 'numpy', - 'requests', 'paramiko', + 'pil', 'pkg_resources', 'pluginbase', + 'qrcode', + 'reportlab', + 'requests', 'smoke_zephyr', - 'win32api', + 'tzlocal', 'websocket', + 'win32api', 'xlsxwriter', + 'yaml', ], excludes=['jinja2.asyncfilters', 'jinja2.asyncsupport'], # not supported with python 3.4 ) From 4d5c929d565f0948b29f3037d0e316332ac58b24 Mon Sep 17 00:00:00 2001 From: Erik Daguerre Date: Tue, 21 Nov 2017 17:05:06 -0500 Subject: [PATCH 35/38] Updated plugin install sensitivity --- king_phisher/client/windows/plugin_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/king_phisher/client/windows/plugin_manager.py b/king_phisher/client/windows/plugin_manager.py index e302b735..c1fa2fa4 100644 --- a/king_phisher/client/windows/plugin_manager.py +++ b/king_phisher/client/windows/plugin_manager.py @@ -278,7 +278,7 @@ def _add_plugins_to_tree(self, catalog_id, repo, store, parent, plugin_list): version=self._get_version_or_upgrade(plugin_name, plugin_list[plugin]['version']), visible_enabled=True, visible_installed=True, - sensitive_installed=True, + sensitive_installed=self.catalog_plugins.is_compatible(catalog_id, repo.id, plugin), type=_ROW_TYPE_PLUGIN )) gui_utilities.glib_idle_add_once(self._store_extend, store, parent, models) From ccef30bf0846fd53a7acf5247f9754395b89be60 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 22 Nov 2017 08:39:53 -0500 Subject: [PATCH 36/38] More documentation updates for v1.9 --- docs/source/change_log.rst | 6 +++++- docs/source/development/release_steps.rst | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/change_log.rst b/docs/source/change_log.rst index 775b5c96..f123c459 100644 --- a/docs/source/change_log.rst +++ b/docs/source/change_log.rst @@ -10,10 +10,13 @@ Version 1.x.x Version 1.9.x ^^^^^^^^^^^^^ -*In Progress* +Released :release:`1.9.0` on November 22nd, 2017 * Support resetting plugins options to their respective defaults * Moved Office 2007+ metadata removal to a new plugin +* Added support for installing plugins from remote sources through the UI +* Added timeout support for SPF DNS queries +* Support for installing on Arch Linux * Multiple server improvements @@ -21,6 +24,7 @@ Version 1.9.x * Support using an include directive in the server configuration file * Added a ``request-handle`` signal for custom HTTP request handlers * Removed ``address`` support from the server config in favor of ``addresses`` + * Support ``login`` as an alias of the ``username`` parameter for credentials Version 1.8.0 ^^^^^^^^^^^^^ diff --git a/docs/source/development/release_steps.rst b/docs/source/development/release_steps.rst index 291f5493..86859237 100644 --- a/docs/source/development/release_steps.rst +++ b/docs/source/development/release_steps.rst @@ -23,7 +23,7 @@ Release Steps #. Upload the final Windows build #. Insert the changes from the change log - #. Insert the MD5 and SHA1 hashes of the Windows build + #. Insert the MD5, SHA1, SHA512 hashes of the Windows build #. Update the Docker build #. Publicize the release From e5dad5abf481cf7011d57ac57af6da4c0475a211 Mon Sep 17 00:00:00 2001 From: Erik Daguerre Date: Wed, 22 Nov 2017 08:49:28 -0500 Subject: [PATCH 37/38] Added king_phisher data folder to cx_freeze --- tools/development/cx_freeze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/development/cx_freeze.py b/tools/development/cx_freeze.py index b9f87db3..c8a03ce0 100644 --- a/tools/development/cx_freeze.py +++ b/tools/development/cx_freeze.py @@ -157,6 +157,7 @@ include_files.append((matplotlib.get_data_path(), 'mpl-data')) include_files.append((basemap.basemap_datadir, 'mpl-basemap-data')) include_files.append(('data/client/king_phisher', 'king_phisher')) +include_files.append(('data/king_phisher', 'king_phisher')) include_files.append((pytz.__path__[0], 'pytz')) include_files.append((requests.__path__[0], 'requests')) From 95016cce933e50386a1f39931edc3c8556b3a0af Mon Sep 17 00:00:00 2001 From: Erik Daguerre Date: Wed, 22 Nov 2017 15:15:08 -0500 Subject: [PATCH 38/38] Version 1.9.0 Final --- king_phisher/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/king_phisher/version.py b/king_phisher/version.py index 900e7cfd..8fb116cd 100644 --- a/king_phisher/version.py +++ b/king_phisher/version.py @@ -67,7 +67,7 @@ def get_revision(): version_info = collections.namedtuple('version_info', ('major', 'minor', 'micro'))(1, 9, 0) """A tuple representing the version information in the format ('major', 'minor', 'micro')""" -version_label = 'beta5' +version_label = '' """A version label such as alpha or beta.""" version = "{0}.{1}.{2}".format(version_info.major, version_info.minor, version_info.micro)