From b5a383a2e1d99e50b76db2b55f19ba285b1bf331 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Wed, 25 Oct 2017 00:05:27 -0400 Subject: [PATCH] Navbar links (#427) * Adding config.json concept in lieu of config.html * Add links to the admin menubar from a plugin * Add links to the user navigation menubar from plugin * Add tests for navbar links * Closes #423 --- CTFd/admin/__init__.py | 7 +++- CTFd/plugins/__init__.py | 51 ++++++++++++++++++++++++ CTFd/themes/admin/templates/base.html | 29 ++++++++++---- CTFd/themes/original/templates/base.html | 10 +++++ CTFd/utils.py | 29 ++++++++++++-- tests/test_plugin_utils.py | 49 ++++++++++++++++++++++- 6 files changed, 162 insertions(+), 13 deletions(-) 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
  • -
  • - - -
  • + + {% 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 %} +
  • + + +
  • + {% 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)