Repository: CTFd/CTFd

Issue:
Mechanism for registering new admin links on the navbar



Discussion:

Resolving patch:
diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py
--- 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
--- a/CTFd/plugins/__init__.py
+++ b/CTFd/plugins/__init__.py
@@ -2,6 +2,7 @@
 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 @@
 )
 
 
+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/utils.py b/CTFd/utils.py
--- a/CTFd/utils.py
+++ b/CTFd/utils.py
@@ -21,6 +21,7 @@
 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():


Test patch:
diff --git a/tests/test_plugin_utils.py b/tests/test_plugin_utils.py
--- a/tests/test_plugin_utils.py
+++ b/tests/test_plugin_utils.py
@@ -8,7 +8,11 @@
     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)