Repository: CTFd/CTFd
Pull request #397 (2017-09-30 00:55:07)

Adding functions to register plugin assets
Adds functions to make it easier for plugins to register assets. 


Patch:
diff --git a/CTFd/plugins/__init__.py b/CTFd/plugins/__init__.py
index 39508e89..fd8b181b 100644
--- a/CTFd/plugins/__init__.py
+++ b/CTFd/plugins/__init__.py
@@ -2,8 +2,60 @@ import glob
 import importlib
 import os
 
+from flask.helpers import safe_join
+from flask import send_file, send_from_directory, abort
+from CTFd.utils import admins_only as admins_only_wrapper
+
+
+def register_plugin_assets_directory(app, base_path, admins_only=False):
+    """
+    Registers a directory to serve assets
+
+    :param app: A CTFd application
+    :param string base_path: The path to the directory
+    :param boolean admins_only: Whether or not the assets served out of the directory should be accessible to the public
+    :return:
+    """
+    base_path = base_path.strip('/')
+
+    def assets_handler(path):
+        return send_from_directory(base_path, path)
+
+    if admins_only:
+        asset_handler = admins_only_wrapper(assets_handler)
+
+    rule = '/' + base_path + '/<path:path>'
+    app.add_url_rule(rule=rule, endpoint=base_path, view_func=assets_handler)
+
+
+def register_plugin_asset(app, asset_path, admins_only=False):
+    """
+    Registers an file path to be served by CTFd
+
+    :param app: A CTFd application
+    :param string asset_path: The path to the asset file
+    :param boolean admins_only: Whether or not this file should be accessible to the public
+    :return:
+    """
+    asset_path = asset_path.strip('/')
+
+    def asset_handler():
+        return send_file(asset_path)
+
+    if admins_only:
+        asset_handler = admins_only_wrapper(asset_handler)
+    rule = '/' + asset_path
+    app.add_url_rule(rule=rule, endpoint=asset_path, view_func=asset_handler)
+
 
 def init_plugins(app):
+    """
+    Searches for the load function in modules in the CTFd/plugins folder. This function is called with the current CTFd
+    app as a parameter. This allows CTFd plugins to modify CTFd's behavior.
+
+    :param app: A CTFd application
+    :return:
+    """
     modules = glob.glob(os.path.dirname(__file__) + "/*")
     blacklist = {'keys', 'challenges', '__pycache__'}
     for module in modules:


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
index c77943cb..d96d9d37 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -17,19 +17,24 @@ def create_ctfd(ctf_name="CTFd", name="admin", email="admin@ctfd.io", password="
     app = create_app('CTFd.config.TestingConfig')
 
     if setup:
-        with app.app_context():
-            with app.test_client() as client:
-                data = {}
-                r = client.get('/setup')  # Populate session with nonce
-                with client.session_transaction() as sess:
-                    data = {
-                        "ctf_name": ctf_name,
-                        "name": name,
-                        "email": email,
-                        "password": password,
-                        "nonce": sess.get('nonce')
-                    }
-                client.post('/setup', data=data)
+        app = setup_ctfd(app, ctf_name, name, email, password)
+    return app
+
+
+def setup_ctfd(app, ctf_name="CTFd", name="admin", email="admin@ctfd.io", password="password"):
+    with app.app_context():
+        with app.test_client() as client:
+            data = {}
+            r = client.get('/setup')  # Populate session with nonce
+            with client.session_transaction() as sess:
+                data = {
+                    "ctf_name": ctf_name,
+                    "name": name,
+                    "email": email,
+                    "password": password,
+                    "nonce": sess.get('nonce')
+                }
+            client.post('/setup', data=data)
     return app
 
 
diff --git a/tests/test_plugin_utils.py b/tests/test_plugin_utils.py
new file mode 100644
index 00000000..63362b8d
--- /dev/null
+++ b/tests/test_plugin_utils.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from tests.helpers import *
+from CTFd.models import ip2long, long2ip
+from CTFd.plugins import register_plugin_assets_directory, register_plugin_asset
+from freezegun import freeze_time
+from mock import patch
+import json
+import six
+
+
+def test_register_plugin_asset():
+    """Test that plugin asset registration works"""
+    app = create_ctfd(setup=False)
+    register_plugin_asset(app, asset_path='/plugins/__init__.py')
+    app = setup_ctfd(app)
+    with app.app_context():
+        with app.test_client() as client:
+            r = client.get('/plugins/__init__.py')
+            assert len(r.get_data(as_text=True)) > 0
+            assert r.status_code == 200
+    destroy_ctfd(app)
+
+
+def test_register_plugin_assets_directory():
+    """Test that plugin asset directory registration works"""
+    app = create_ctfd(setup=False)
+    register_plugin_assets_directory(app, base_path='/plugins/')
+    app = setup_ctfd(app)
+    with app.app_context():
+        with app.test_client() as client:
+            r = client.get('/plugins/__init__.py')
+            assert len(r.get_data(as_text=True)) > 0
+            assert r.status_code == 200
+            r = client.get('/plugins/challenges/__init__.py')
+            assert len(r.get_data(as_text=True)) > 0
+            assert r.status_code == 200
+    destroy_ctfd(app)