diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py index 44c2787..c5efaf7 100644 --- a/pwnagotchi/plugins/__init__.py +++ b/pwnagotchi/plugins/__init__.py @@ -6,7 +6,7 @@ import logging default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default") loaded = {} - +database = {} class Plugin: @classmethod @@ -18,6 +18,26 @@ class Plugin: logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance)) loaded[plugin_name] = plugin_instance +def toggle_plugin(name, enable=True): + """ + Load or unload a plugin + + returns True if changed, otherwise False + """ + global loaded, database + if not enable and name in loaded: + if getattr(loaded[name], 'on_unload', None): + loaded[name].on_unload() + del loaded[name] + return True + + if enable and name in database and name not in loaded: + load_from_file(database[name]) + one(name, 'loaded') + return True + + return False + def on(event_name, *args, **kwargs): for plugin_name, plugin in loaded.items(): @@ -48,10 +68,11 @@ def load_from_file(filename): def load_from_path(path, enabled=()): - global loaded + global loaded, database logging.debug("loading plugins from %s - enabled: %s" % (path, enabled)) for filename in glob.glob(os.path.join(path, "*.py")): plugin_name = os.path.basename(filename.replace(".py", "")) + database[plugin_name] = filename if plugin_name in enabled: try: load_from_file(filename) diff --git a/pwnagotchi/plugins/default/bt-tether.py b/pwnagotchi/plugins/default/bt-tether.py index 6acaf27..5f986f8 100644 --- a/pwnagotchi/plugins/default/bt-tether.py +++ b/pwnagotchi/plugins/default/bt-tether.py @@ -466,7 +466,11 @@ class BTTether(plugins.Plugin): logging.info("BT-TETHER: Successfully loaded ...") self.ready = True + def on_unload(self): + self.ui.remove_element('bluetooth') + def on_ui_setup(self, ui): + self.ui = ui ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0), label_font=fonts.Bold, text_font=fonts.Medium)) diff --git a/pwnagotchi/plugins/default/example.py b/pwnagotchi/plugins/default/example.py index 035a30d..37eccb4 100644 --- a/pwnagotchi/plugins/default/example.py +++ b/pwnagotchi/plugins/default/example.py @@ -25,6 +25,10 @@ class Example(plugins.Plugin): def on_loaded(self): logging.warning("WARNING: this plugin should be disabled! options = " % self.options) + # called before the plugin is unloaded + def on_unload(self): + pass + # called hen there's internet connectivity def on_internet_available(self, agent): pass diff --git a/pwnagotchi/ui/web/handler.py b/pwnagotchi/ui/web/handler.py index c11b173..788a3d1 100644 --- a/pwnagotchi/ui/web/handler.py +++ b/pwnagotchi/ui/web/handler.py @@ -179,16 +179,19 @@ class Handler: def plugins(self, name, subpath): if name is None: - # show plugins overview - abort(404) + return render_template('plugins.html', loaded=plugins.loaded, database=plugins.database) + + if name == 'toggle' and request.method == 'POST': + checked = True if 'enabled' in request.form else False + return 'success' if plugins.toggle_plugin(request.form['plugin'], checked) else 'failed' + + if name in plugins.loaded and plugins.loaded[name] is not None and hasattr(plugins.loaded[name], 'on_webhook'): + try: + return plugins.loaded[name].on_webhook(subpath, request) + except Exception: + abort(500) else: - if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'): - try: - return plugins.loaded[name].on_webhook(subpath, request) - except Exception: - abort(500) - else: - abort(404) + abort(404) # serve a message and shuts down the unit def shutdown(self): diff --git a/pwnagotchi/ui/web/static/css/style.css b/pwnagotchi/ui/web/static/css/style.css index 8acc267..a005a1d 100644 --- a/pwnagotchi/ui/web/static/css/style.css +++ b/pwnagotchi/ui/web/static/css/style.css @@ -31,4 +31,37 @@ a.read { p.messagebody { padding: 1em; -} \ No newline at end of file +} + +li.navitem { + width: 16.66% !important; + clear: none !important; +} + +/* Custom indentations are needed because the length of custom labels differs from + the length of the standard labels */ +.custom-size-flipswitch.ui-flipswitch .ui-btn.ui-flipswitch-on { + text-indent: -5.9em; +} + +.custom-size-flipswitch.ui-flipswitch .ui-flipswitch-off { + text-indent: 0.5em; +} + +/* Custom widths are needed because the length of custom labels differs from + the length of the standard labels */ +.custom-size-flipswitch.ui-flipswitch { + width: 8.875em; +} + +.custom-size-flipswitch.ui-flipswitch.ui-flipswitch-active { + padding-left: 7em; + width: 1.875em; +} + +@media (min-width: 28em) { + /*Repeated from rule .ui-flipswitch above*/ + .ui-field-contain > label + .custom-size-flipswitch.ui-flipswitch { + width: 1.875em; + } +} diff --git a/pwnagotchi/ui/web/templates/base.html b/pwnagotchi/ui/web/templates/base.html index c39e080..be2d618 100644 --- a/pwnagotchi/ui/web/templates/base.html +++ b/pwnagotchi/ui/web/templates/base.html @@ -47,6 +47,7 @@ ( '/inbox/new', 'new', 'mail', 'New' ), ( '/inbox/profile', 'profile', 'info', 'Profile' ), ( '/inbox/peers', 'peers', 'user', 'Peers' ), + ( '/plugins', 'plugins', 'grid', 'Plugins' ), ] %} {% set active_page = active_page|default('inbox') %} @@ -54,7 +55,7 @@ <div data-role="navbar" data-iconpos="left"> <ul> {% for href, id, icon, caption in navigation %} - <li> + <li class="navitem"> <a href="{{ href }}" id="{{ id }}" data-icon="{{ icon }}" class="{{ 'ui-btn-active' if active_page == id }}">{{ caption }}</a> </li> {% endfor %} diff --git a/pwnagotchi/ui/web/templates/plugins.html b/pwnagotchi/ui/web/templates/plugins.html new file mode 100644 index 0000000..e96d244 --- /dev/null +++ b/pwnagotchi/ui/web/templates/plugins.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% set active_page = "plugins" %} + +{% block title %} +Plugins +{% endblock %} + +{% block script %} +$(function(){ + $("input[type=checkbox]").change(function(e) { + var checkbox = $(this); + var form = checkbox.closest("form"); + var url = form.attr('action'); + + $.ajax({ + type: "POST", + url: url, + data: form.serialize(), + success: function(data) { + if( data.indexOf('failed') != -1 ) { + alert('Could not be toggled.'); + } + } + }); + }); +}); +{% endblock %} +{% block content %} +<div style="padding: 1em"> + {% for name in database.keys() %} + <h4>{{name}}</h4> + <form method="POST" action="/plugins/toggle"> + <input type="checkbox" data-role="flipswitch" name="enabled" id="flip-checkbox-{{name}}" data-on-text="Enabled" data-off-text="Disabled" data-wrapper-class="custom-size-flipswitch" {% if name in loaded %} checked {% endif %}> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> + <input type="hidden" name="plugin" value="{{ name }}"/> + </form> + {% endfor %} +</div> +{% endblock %}