Skip to content

Commit

Permalink
Navbar links (CTFd#427)
Browse files Browse the repository at this point in the history
* 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 CTFd#423
  • Loading branch information
ColdHeat authored Oct 25, 2017
1 parent 710ce6d commit b5a383a
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 13 deletions.
7 changes: 6 additions & 1 deletion CTFd/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions CTFd/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
29 changes: 21 additions & 8 deletions CTFd/themes/admin/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,27 @@
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
<li><a href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
<li><a href="{{ request.script_root }}/admin/config">Config</a></li>
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for plugin in get_configurable_plugins() %}
<li><a href="{{ request.script_root }}/admin/plugins/{{ plugin }}">{{ plugin }}</a></li>
{% endfor %}
</ul>
</li>

{% set plugin_menu = get_admin_plugin_menu_bar() %}
{% set plugins = get_configurable_plugins() %}
{% if plugin_menu or plugins %}
<li><a style="padding-left:0px;padding-right:0px;">|</a></li>

{% for menu in plugin_menu %}
<li><a href="{{ request.script_root }}{{ menu.route }}">{{ menu.name }}</a></li>
{% endfor %}

{% if plugins %}
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for plugin in plugins %}
<li><a href="{{ request.script_root }}{{ plugin.route }}">{{ plugin.name }}</a></li>
{% endfor %}
</ul>
</li>
{% endif %}
{% endif %}
</ul>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions CTFd/themes/original/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
{% for page in pages() %}
<li><a href="{{ request.script_root }}/{{ page.route }}">{{ page.route|title }}</a></li>
{% endfor %}

{% set page_menu = get_user_page_menu_bar() %}
{% for menu in page_menu %}
{% if menu.route.startswith('http://') or menu.route.startswith('https://') %}
<li><a href="{{ menu.route }}">{{ menu.name }}</a></li>
{% else %}
<li><a href="{{ request.script_root }}{{ menu.route }}">{{ menu.name }}</a></li>
{% endif %}
{% endfor %}

<li><a href="{{ request.script_root }}/teams">Teams</a></li>
{% if not hide_scores() %}
<li><a href="{{ request.script_root }}/scoreboard">Scoreboard</a></li>
Expand Down
29 changes: 26 additions & 3 deletions CTFd/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
49 changes: 48 additions & 1 deletion tests/test_plugin_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

0 comments on commit b5a383a

Please sign in to comment.