mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 14:04:20 +01:00
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
This commit is contained in:
@@ -38,7 +38,12 @@ def admin_view():
|
|||||||
@admins_only
|
@admins_only
|
||||||
def admin_plugin_config(plugin):
|
def admin_plugin_config(plugin):
|
||||||
if request.method == 'GET':
|
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()
|
config = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read()
|
||||||
return render_template_string(config)
|
return render_template_string(config)
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import glob
|
|||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
from flask.helpers import safe_join
|
from flask.helpers import safe_join
|
||||||
from flask import current_app as app, send_file, send_from_directory, abort
|
from flask import current_app as app, send_file, send_from_directory, abort
|
||||||
from CTFd.utils import (
|
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):
|
def register_plugin_assets_directory(app, base_path, admins_only=False):
|
||||||
"""
|
"""
|
||||||
Registers a directory to serve assets
|
Registers a directory to serve assets
|
||||||
@@ -76,6 +82,48 @@ def register_plugin_stylesheet(*args, **kwargs):
|
|||||||
utils_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):
|
def init_plugins(app):
|
||||||
"""
|
"""
|
||||||
Searches for the load function in modules in the CTFd/plugins folder. This function is called with the current CTFd
|
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 = importlib.import_module(module, package='CTFd.plugins')
|
||||||
module.load(app)
|
module.load(app)
|
||||||
print(" * Loaded module, %s" % module)
|
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)
|
||||||
|
|||||||
@@ -50,14 +50,27 @@
|
|||||||
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
|
<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/statistics">Statistics</a></li>
|
||||||
<li><a href="{{ request.script_root }}/admin/config">Config</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>
|
{% set plugin_menu = get_admin_plugin_menu_bar() %}
|
||||||
<ul class="dropdown-menu">
|
{% set plugins = get_configurable_plugins() %}
|
||||||
{% for plugin in get_configurable_plugins() %}
|
{% if plugin_menu or plugins %}
|
||||||
<li><a href="{{ request.script_root }}/admin/plugins/{{ plugin }}">{{ plugin }}</a></li>
|
<li><a style="padding-left:0px;padding-right:0px;">|</a></li>
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
{% for menu in plugin_menu %}
|
||||||
</li>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,6 +45,16 @@
|
|||||||
{% for page in pages() %}
|
{% for page in pages() %}
|
||||||
<li><a href="{{ request.script_root }}/{{ page.route }}">{{ page.route|title }}</a></li>
|
<li><a href="{{ request.script_root }}/{{ page.route }}">{{ page.route|title }}</a></li>
|
||||||
{% endfor %}
|
{% 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>
|
<li><a href="{{ request.script_root }}/teams">Teams</a></li>
|
||||||
{% if not hide_scores() %}
|
{% if not hide_scores() %}
|
||||||
<li><a href="{{ request.script_root }}/scoreboard">Scoreboard</a></li>
|
<li><a href="{{ request.script_root }}/scoreboard">Scoreboard</a></li>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import datafreeze
|
|||||||
import zipfile
|
import zipfile
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from flask import current_app as app, request, redirect, url_for, session, render_template, abort
|
from flask import current_app as app, request, redirect, url_for, session, render_template, abort
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
@@ -407,9 +408,31 @@ def get_themes():
|
|||||||
|
|
||||||
|
|
||||||
def get_configurable_plugins():
|
def get_configurable_plugins():
|
||||||
dir = os.path.join(app.root_path, 'plugins')
|
Plugin = namedtuple('Plugin', ['name', 'route'])
|
||||||
return [name for name in os.listdir(dir)
|
|
||||||
if os.path.isfile(os.path.join(dir, name, 'config.html'))]
|
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():
|
def get_registered_scripts():
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ from CTFd.plugins import (
|
|||||||
register_plugin_asset,
|
register_plugin_asset,
|
||||||
register_plugin_script,
|
register_plugin_script,
|
||||||
register_plugin_stylesheet,
|
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 freezegun import freeze_time
|
||||||
from mock import patch
|
from mock import patch
|
||||||
@@ -98,3 +102,46 @@ def test_register_plugin_stylesheet():
|
|||||||
assert '/fake/stylesheet/path.css' in output
|
assert '/fake/stylesheet/path.css' in output
|
||||||
assert 'http://ctfd.io/fake/stylesheet/path.css' in output
|
assert 'http://ctfd.io/fake/stylesheet/path.css' in output
|
||||||
destroy_ctfd(app)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user