diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py
index 526642c1..d153c42e 100644
--- a/CTFd/admin/__init__.py
+++ b/CTFd/admin/__init__.py
@@ -38,7 +38,12 @@ def admin_view():
@admins_only
def admin_plugin_config(plugin):
if request.method == 'GET':
- if plugin in utils.get_configurable_plugins():
+ plugins_path = os.path.join(app.root_path, 'plugins')
+
+ config_html_plugins = [name for name in os.listdir(plugins_path)
+ if os.path.isfile(os.path.join(plugins_path, name, 'config.html'))]
+
+ if plugin in config_html_plugins:
config = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read()
return render_template_string(config)
abort(404)
diff --git a/CTFd/plugins/__init__.py b/CTFd/plugins/__init__.py
index 9457a695..b5b8d9b4 100644
--- a/CTFd/plugins/__init__.py
+++ b/CTFd/plugins/__init__.py
@@ -2,6 +2,7 @@ import glob
import importlib
import os
+from collections import namedtuple
from flask.helpers import safe_join
from flask import current_app as app, send_file, send_from_directory, abort
from CTFd.utils import (
@@ -12,6 +13,11 @@ from CTFd.utils import (
)
+Menu = namedtuple('Menu', ['name', 'route'])
+ADMIN_PLUGIN_MENU_BAR = []
+USER_PAGE_MENU_BAR = []
+
+
def register_plugin_assets_directory(app, base_path, admins_only=False):
"""
Registers a directory to serve assets
@@ -76,6 +82,48 @@ def register_plugin_stylesheet(*args, **kwargs):
utils_register_plugin_stylesheet(*args, **kwargs)
+def register_admin_plugin_menu_bar(name, route):
+ """
+ Registers links on the Admin Panel menubar/navbar
+
+ :param name: A string that is shown on the navbar HTML
+ :param route: A string that is the href used by the link
+ :return:
+ """
+ am = Menu(name=name, route=route)
+ ADMIN_PLUGIN_MENU_BAR.append(am)
+
+
+def get_admin_plugin_menu_bar():
+ """
+ Access the list used to store the plugin menu bar
+
+ :return: Returns a list of Menu namedtuples. They have name, and route attributes.
+ """
+ return ADMIN_PLUGIN_MENU_BAR
+
+
+def register_user_page_menu_bar(name, route):
+ """
+ Registers links on the User side menubar/navbar
+
+ :param name: A string that is shown on the navbar HTML
+ :param route: A string that is the href used by the link
+ :return:
+ """
+ p = Menu(name=name, route=route)
+ USER_PAGE_MENU_BAR.append(p)
+
+
+def get_user_page_menu_bar():
+ """
+ Access the list used to store the user page menu bar
+
+ :return: Returns a list of Menu namedtuples. They have name, and route attributes.
+ """
+ return USER_PAGE_MENU_BAR
+
+
def init_plugins(app):
"""
Searches for the load function in modules in the CTFd/plugins folder. This function is called with the current CTFd
@@ -93,3 +141,6 @@ def init_plugins(app):
module = importlib.import_module(module, package='CTFd.plugins')
module.load(app)
print(" * Loaded module, %s" % module)
+
+ app.jinja_env.globals.update(get_admin_plugin_menu_bar=get_admin_plugin_menu_bar)
+ app.jinja_env.globals.update(get_user_page_menu_bar=get_user_page_menu_bar)
diff --git a/CTFd/themes/admin/templates/base.html b/CTFd/themes/admin/templates/base.html
index 7c41fa95..3c6cdc1f 100644
--- a/CTFd/themes/admin/templates/base.html
+++ b/CTFd/themes/admin/templates/base.html
@@ -50,14 +50,27 @@
Challenges
Statistics
Config
-
- Plugins
-
-
+
+ {% set plugin_menu = get_admin_plugin_menu_bar() %}
+ {% set plugins = get_configurable_plugins() %}
+ {% if plugin_menu or plugins %}
+ |
+
+ {% for menu in plugin_menu %}
+ {{ menu.name }}
+ {% endfor %}
+
+ {% if plugins %}
+
+ Plugins
+
+
+ {% endif %}
+ {% endif %}
diff --git a/CTFd/themes/original/templates/base.html b/CTFd/themes/original/templates/base.html
index c0dc4bfa..8898a6ce 100644
--- a/CTFd/themes/original/templates/base.html
+++ b/CTFd/themes/original/templates/base.html
@@ -45,6 +45,16 @@
{% for page in pages() %}
{{ page.route|title }}
{% endfor %}
+
+ {% set page_menu = get_user_page_menu_bar() %}
+ {% for menu in page_menu %}
+ {% if menu.route.startswith('http://') or menu.route.startswith('https://') %}
+ {{ menu.name }}
+ {% else %}
+ {{ menu.name }}
+ {% endif %}
+ {% endfor %}
+
Teams
{% if not hide_scores() %}
Scoreboard
diff --git a/CTFd/utils.py b/CTFd/utils.py
index 7f8747de..a785af85 100644
--- a/CTFd/utils.py
+++ b/CTFd/utils.py
@@ -21,6 +21,7 @@ import datafreeze
import zipfile
import io
+from collections import namedtuple
from email.mime.text import MIMEText
from flask import current_app as app, request, redirect, url_for, session, render_template, abort
from flask_caching import Cache
@@ -407,9 +408,31 @@ def get_themes():
def get_configurable_plugins():
- dir = os.path.join(app.root_path, 'plugins')
- return [name for name in os.listdir(dir)
- if os.path.isfile(os.path.join(dir, name, 'config.html'))]
+ Plugin = namedtuple('Plugin', ['name', 'route'])
+
+ plugins_path = os.path.join(app.root_path, 'plugins')
+ plugin_directories = os.listdir(plugins_path)
+
+ plugins = []
+
+ for dir in plugin_directories:
+ if os.path.isfile(os.path.join(plugins_path, dir, 'config.json')):
+ path = os.path.join(plugins_path, dir, 'config.json')
+ with open(path) as f:
+ plugin_json_data = json.loads(f.read())
+ p = Plugin(
+ name=plugin_json_data.get('name'),
+ route=plugin_json_data.get('route')
+ )
+ plugins.append(p)
+ elif os.path.isfile(os.path.join(plugins_path, dir, 'config.html')):
+ p = Plugin(
+ name=dir,
+ route='/admin/plugins/{}'.format(dir)
+ )
+ plugins.append(p)
+
+ return plugins
def get_registered_scripts():
diff --git a/tests/test_plugin_utils.py b/tests/test_plugin_utils.py
index 942f432a..dd256961 100644
--- a/tests/test_plugin_utils.py
+++ b/tests/test_plugin_utils.py
@@ -8,7 +8,11 @@ from CTFd.plugins import (
register_plugin_asset,
register_plugin_script,
register_plugin_stylesheet,
- override_template
+ override_template,
+ register_admin_plugin_menu_bar,
+ get_admin_plugin_menu_bar,
+ register_user_page_menu_bar,
+ get_user_page_menu_bar
)
from freezegun import freeze_time
from mock import patch
@@ -98,3 +102,46 @@ def test_register_plugin_stylesheet():
assert '/fake/stylesheet/path.css' in output
assert 'http://ctfd.io/fake/stylesheet/path.css' in output
destroy_ctfd(app)
+
+
+def test_register_admin_plugin_menu_bar():
+ """
+ Test that register_admin_plugin_menu_bar() properly inserts into HTML and get_admin_plugin_menu_bar()
+ returns the proper list.
+ """
+ app = create_ctfd()
+ with app.app_context():
+ register_admin_plugin_menu_bar(name='test_admin_plugin_name', route='/test_plugin')
+
+ client = login_as_user(app, name="admin", password="password")
+ r = client.get('/admin/graphs')
+ output = r.get_data(as_text=True)
+ assert '/test_plugin' in output
+ assert 'test_admin_plugin_name' in output
+
+ menu_item = get_admin_plugin_menu_bar()[0]
+ assert menu_item.name == 'test_admin_plugin_name'
+ assert menu_item.route == '/test_plugin'
+ destroy_ctfd(app)
+
+
+def test_register_user_page_menu_bar():
+ """
+ Test that the register_user_page_menu_bar() properly inserts into HTML and get_user_page_menu_bar() returns the
+ proper list.
+ """
+ app = create_ctfd()
+ with app.app_context():
+ register_user_page_menu_bar(name='test_user_menu_link', route='/test_user_href')
+
+ client = login_as_user(app)
+ r = client.get('/')
+
+ output = r.get_data(as_text=True)
+ assert '/test_user_href' in output
+ assert 'test_user_menu_link' in output
+
+ menu_item = get_user_page_menu_bar()[0]
+ assert menu_item.name == 'test_user_menu_link'
+ assert menu_item.route == '/test_user_href'
+ destroy_ctfd(app)