Repository: CTFd/CTFd (python)

Issue:
Create admin panel only scripts + stylesheet plugin interfaces
```
def register_script(url):
    SCRIPTS.append(url)


def register_stylesheet(url):
    STYLESHEETS.append(url)


def get_registered_scripts():
    return SCRIPTS


def get_registered_stylesheets():
    return STYLESHEETS
```

The above code exists for themes but the admin panel doesn't make use of it and it really shouldn't. We should make functions to add things specifically to the admin panel. 

Pull request description:
Adds plugin functions to register javascript and CSS in the admin panel
* Adds plugin functions to register javascript and CSS in the admin panel
* Move global plugin script/stylesheet lists into application factory specific lists
* Closes #804

Patch:
diff --git a/CTFd/plugins/__init__.py b/CTFd/plugins/__init__.py
index 60559e62..223832cb 100644
--- a/CTFd/plugins/__init__.py
+++ b/CTFd/plugins/__init__.py
@@ -3,20 +3,19 @@ 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.decorators import admins_only as admins_only_wrapper
 from CTFd.utils.plugins import (
     override_template as utils_override_template,
     register_script as utils_register_plugin_script,
     register_stylesheet as utils_register_plugin_stylesheet,
+    register_admin_script as utils_register_admin_plugin_script,
+    register_admin_stylesheet as utils_register_admin_plugin_stylesheet
 )
 from CTFd.utils.config.pages import get_pages
 
 
 Menu = namedtuple('Menu', ['title', 'route'])
-ADMIN_PLUGIN_MENU_BAR = []
-USER_PAGE_MENU_BAR = []
 
 
 def register_plugin_assets_directory(app, base_path, admins_only=False):
@@ -83,6 +82,26 @@ def register_plugin_stylesheet(*args, **kwargs):
     utils_register_plugin_stylesheet(*args, **kwargs)
 
 
+def register_admin_plugin_script(*args, **kwargs):
+    """
+    Adds a given script to the base.html of the admin theme which all admin pages inherit from
+    :param args:
+    :param kwargs:
+    :return:
+    """
+    utils_register_admin_plugin_script(*args, **kwargs)
+
+
+def register_admin_plugin_stylesheet(*args, **kwargs):
+    """
+    Adds a given stylesheet to the base.html of the admin theme which all admin pages inherit from
+    :param args:
+    :param kwargs:
+    :return:
+    """
+    utils_register_admin_plugin_stylesheet(*args, **kwargs)
+
+
 def register_admin_plugin_menu_bar(title, route):
     """
     Registers links on the Admin Panel menubar/navbar
@@ -92,7 +111,7 @@ def register_admin_plugin_menu_bar(title, route):
     :return:
     """
     am = Menu(title=title, route=route)
-    ADMIN_PLUGIN_MENU_BAR.append(am)
+    app.admin_plugin_menu_bar.append(am)
 
 
 def get_admin_plugin_menu_bar():
@@ -101,7 +120,7 @@ def get_admin_plugin_menu_bar():
 
     :return: Returns a list of Menu namedtuples. They have name, and route attributes.
     """
-    return ADMIN_PLUGIN_MENU_BAR
+    return app.admin_plugin_menu_bar
 
 
 def register_user_page_menu_bar(title, route):
@@ -113,7 +132,7 @@ def register_user_page_menu_bar(title, route):
     :return:
     """
     p = Menu(title=title, route=route)
-    USER_PAGE_MENU_BAR.append(p)
+    app.plugin_menu_bar.append(p)
 
 
 def get_user_page_menu_bar():
@@ -122,7 +141,7 @@ def get_user_page_menu_bar():
 
     :return: Returns a list of Menu namedtuples. They have name, and route attributes.
     """
-    return get_pages() + USER_PAGE_MENU_BAR
+    return get_pages() + app.plugin_menu_bar
 
 
 def bypass_csrf_protection(f):
@@ -146,6 +165,14 @@ def init_plugins(app):
     :param app: A CTFd application
     :return:
     """
+    app.admin_plugin_scripts = []
+    app.admin_plugin_stylesheets = []
+    app.plugin_scripts = []
+    app.plugin_stylesheets = []
+
+    app.admin_plugin_menu_bar = []
+    app.plugin_menu_bar = []
+
     if app.config.get('SAFE_MODE', False) is False:
         modules = sorted(glob.glob(os.path.dirname(__file__) + "/*"))
         blacklist = {'__pycache__'}
diff --git a/CTFd/themes/admin/templates/base.html b/CTFd/themes/admin/templates/base.html
index f06677a8..4d8cd56a 100644
--- a/CTFd/themes/admin/templates/base.html
+++ b/CTFd/themes/admin/templates/base.html
@@ -25,6 +25,15 @@
 		var user_mode = "{{ get_config('user_mode') }}";
 	</script>
 	{% block stylesheets %} {% endblock %}
+	{% for stylesheet in get_registered_admin_stylesheets() %}
+		{% if stylesheet.startswith('http') %}
+			<link rel="stylesheet" type="text/css" href="{{ stylesheet }}">
+		{% elif request.script_root %}
+			<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/{{ stylesheet }}">
+		{% else %}
+			<link rel="stylesheet" type="text/css" href="{{ stylesheet }}">
+		{% endif %}
+	{% endfor %}
 </head>
 
 <body>
@@ -135,6 +144,16 @@
 	<script src="{{ url_for('views.themes', theme='admin', path='js/ezq.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/style.js') }}"></script>
 	{% block scripts %} {% endblock %}
+
+	{% for script in get_registered_admin_scripts() %}
+		{% if script.startswith('http') %}
+			<script src="{{ script }}"></script>
+		{% elif request.script_root %}
+			<script src="{{ request.script_root }}/{{ script }}"></script>
+		{% else %}
+			<script src="{{ script }}"></script>
+		{% endif %}
+	{% endfor %}
 </body>
 
 </html>
diff --git a/CTFd/utils/initialization/__init__.py b/CTFd/utils/initialization/__init__.py
index a45dbcad..647a499d 100644
--- a/CTFd/utils/initialization/__init__.py
+++ b/CTFd/utils/initialization/__init__.py
@@ -8,7 +8,13 @@ from CTFd.utils import config
 from CTFd.utils.config import can_send_mail, ctf_logo, ctf_name, ctf_theme
 from CTFd.utils.config.pages import get_pages
 
-from CTFd.utils.plugins import get_registered_stylesheets, get_registered_scripts, get_configurable_plugins
+from CTFd.utils.plugins import (
+    get_registered_stylesheets,
+    get_registered_scripts,
+    get_configurable_plugins,
+    get_registered_admin_scripts,
+    get_registered_admin_stylesheets
+)
 
 from CTFd.utils.countries import get_countries, lookup_country_code
 from CTFd.utils.user import authed, get_ip, get_current_user, get_current_team
@@ -45,6 +51,8 @@ def init_template_globals(app):
     app.jinja_env.globals.update(get_configurable_plugins=get_configurable_plugins)
     app.jinja_env.globals.update(get_registered_scripts=get_registered_scripts)
     app.jinja_env.globals.update(get_registered_stylesheets=get_registered_stylesheets)
+    app.jinja_env.globals.update(get_registered_admin_scripts=get_registered_admin_scripts)
+    app.jinja_env.globals.update(get_registered_admin_stylesheets=get_registered_admin_stylesheets)
     app.jinja_env.globals.update(get_config=get_config)
     app.jinja_env.globals.update(generate_account_url=generate_account_url)
     app.jinja_env.globals.update(get_countries=get_countries)
diff --git a/CTFd/utils/plugins/__init__.py b/CTFd/utils/plugins/__init__.py
index 770fc534..8781a3fb 100644
--- a/CTFd/utils/plugins/__init__.py
+++ b/CTFd/utils/plugins/__init__.py
@@ -4,24 +4,36 @@ import os
 import json
 
 
-SCRIPTS = []
-STYLESHEETS = []
-
-
 def register_script(url):
-    SCRIPTS.append(url)
+    app.plugin_scripts.append(url)
 
 
 def register_stylesheet(url):
-    STYLESHEETS.append(url)
+    app.plugin_stylesheets.append(url)
+
+
+def register_admin_script(url):
+    app.admin_plugin_scripts.append(url)
+
+
+def register_admin_stylesheet(url):
+    app.admin_plugin_stylesheets.append(url)
 
 
 def get_registered_scripts():
-    return SCRIPTS
+    return app.plugin_scripts
 
 
 def get_registered_stylesheets():
-    return STYLESHEETS
+    return app.plugin_stylesheets
+
+
+def get_registered_admin_scripts():
+    return app.admin_plugin_scripts
+
+
+def get_registered_admin_stylesheets():
+    return app.admin_plugin_stylesheets
 
 
 def override_template(template, html):


Test patch:
diff --git a/tests/test_plugin_utils.py b/tests/test_plugin_utils.py
index 8bee2d6d..0e28b3a3 100644
--- a/tests/test_plugin_utils.py
+++ b/tests/test_plugin_utils.py
@@ -7,6 +7,8 @@ from CTFd.plugins import (
     register_plugin_asset,
     register_plugin_script,
     register_plugin_stylesheet,
+    register_admin_plugin_script,
+    register_admin_plugin_stylesheet,
     override_template,
     register_admin_plugin_menu_bar,
     get_admin_plugin_menu_bar,
@@ -77,7 +79,7 @@ def test_admin_override_template():
 
 
 def test_register_plugin_script():
-    '''Test that register_plugin_script adds script paths to the core theme when used from a plugin'''
+    """Test that register_plugin_script adds script paths to the core theme when used from a plugin"""
     app = create_ctfd()
     with app.app_context():
         register_plugin_script('/fake/script/path.js')
@@ -91,7 +93,7 @@ def test_register_plugin_script():
 
 
 def test_register_plugin_stylesheet():
-    '''Test that register_plugin_stylesheet adds stylesheet paths to the core theme when used from a plugin'''
+    """Test that register_plugin_stylesheet adds stylesheet paths to the core theme when used from a plugin"""
     app = create_ctfd()
     with app.app_context():
         register_plugin_script('/fake/stylesheet/path.css')
@@ -104,6 +106,34 @@ def test_register_plugin_stylesheet():
     destroy_ctfd(app)
 
 
+def test_register_admin_plugin_script():
+    """Test that register_admin_plugin_script adds script paths to the admin theme when used from a plugin"""
+    app = create_ctfd()
+    with app.app_context():
+        register_admin_plugin_script('/fake/script/path.js')
+        register_admin_plugin_script('http://ctfd.io/fake/script/path.js')
+        with login_as_user(app, name="admin") as client:
+            r = client.get('/admin/statistics')
+            output = r.get_data(as_text=True)
+            assert '/fake/script/path.js' in output
+            assert 'http://ctfd.io/fake/script/path.js' in output
+    destroy_ctfd(app)
+
+
+def test_register_admin_plugin_stylesheet():
+    """Test that register_admin_plugin_stylesheet adds stylesheet paths to the admin theme when used from a plugin"""
+    app = create_ctfd()
+    with app.app_context():
+        register_admin_plugin_stylesheet('/fake/stylesheet/path.css')
+        register_admin_plugin_stylesheet('http://ctfd.io/fake/stylesheet/path.css')
+        with login_as_user(app, name="admin") as client:
+            r = client.get('/admin/statistics')
+            output = r.get_data(as_text=True)
+            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()