diff --git a/CTFd/api/v1/users.py b/CTFd/api/v1/users.py index 269953b5..37ce2dc3 100644 --- a/CTFd/api/v1/users.py +++ b/CTFd/api/v1/users.py @@ -339,7 +339,8 @@ class UserPrivateSolves(Resource): if response.errors: return {"success": False, "errors": response.errors}, 400 - return {"success": True, "data": response.data} + count = len(response.data) + return {"success": True, "data": response.data, "meta": {"count": count}} @users_namespace.route("/me/fails") @@ -358,8 +359,8 @@ class UserPrivateFails(Resource): data = response.data else: data = [] - count = len(response.data) + count = len(response.data) return {"success": True, "data": data, "meta": {"count": count}} @@ -377,7 +378,8 @@ class UserPrivateAwards(Resource): if response.errors: return {"success": False, "errors": response.errors}, 400 - return {"success": True, "data": response.data} + count = len(response.data) + return {"success": True, "data": response.data, "meta": {"count": count}} @users_namespace.route("//solves") @@ -399,7 +401,8 @@ class UserPublicSolves(Resource): if response.errors: return {"success": False, "errors": response.errors}, 400 - return {"success": True, "data": response.data} + count = len(response.data) + return {"success": True, "data": response.data, "meta": {"count": count}} @users_namespace.route("//fails") @@ -423,8 +426,8 @@ class UserPublicFails(Resource): data = response.data else: data = [] - count = len(response.data) + count = len(response.data) return {"success": True, "data": data, "meta": {"count": count}} @@ -446,7 +449,8 @@ class UserPublicAwards(Resource): if response.errors: return {"success": False, "errors": response.errors}, 400 - return {"success": True, "data": response.data} + count = len(response.data) + return {"success": True, "data": response.data, "meta": {"count": count}} @users_namespace.route("//email") diff --git a/CTFd/constants/assets.py b/CTFd/constants/assets.py new file mode 100644 index 00000000..ba505613 --- /dev/null +++ b/CTFd/constants/assets.py @@ -0,0 +1,51 @@ +import os + +from flask import current_app, url_for + +from CTFd.utils import _get_asset_json +from CTFd.utils.config import ctf_theme +from CTFd.utils.helpers import markup + + +class _AssetsWrapper: + def manifest(self): + theme = ctf_theme() + manifest = os.path.join( + current_app.root_path, "themes", theme, "static", "manifest.json" + ) + return _get_asset_json(path=manifest) + + def manifest_css(self): + theme = ctf_theme() + manifest = os.path.join( + current_app.root_path, "themes", theme, "static", "manifest-css.json" + ) + return _get_asset_json(path=manifest) + + def js(self, asset_key): + asset = self.manifest()[asset_key] + entry = asset["file"] + imports = asset.get("imports", []) + html = "" + for i in imports: + # TODO: Needs a better recursive solution + i = self.manifest()[i]["file"] + url = url_for("views.themes_beta", path=i) + html += f'' + url = url_for("views.themes_beta", path=entry) + html += f'' + return markup(html) + + def css(self, asset_key): + asset = self.manifest_css()[asset_key] + entry = asset["file"] + url = url_for("views.themes_beta", path=entry) + return markup(f'') + + def file(self, asset_key): + asset = self.manifest()[asset_key] + entry = asset["file"] + return url_for("views.themes_beta", path=entry) + + +Assets = _AssetsWrapper() diff --git a/CTFd/utils/__init__.py b/CTFd/utils/__init__.py index 1952d860..0a9d2eaf 100644 --- a/CTFd/utils/__init__.py +++ b/CTFd/utils/__init__.py @@ -1,3 +1,4 @@ +import json from enum import Enum import cmarkgfm @@ -26,6 +27,12 @@ def get_app_config(key, default=None): return value +@cache.memoize() +def _get_asset_json(path): + with open(path) as f: + return json.loads(f.read()) + + @cache.memoize() def _get_config(key): config = db.session.execute( diff --git a/CTFd/utils/helpers/__init__.py b/CTFd/utils/helpers/__init__.py index d01b531c..8faa8a56 100644 --- a/CTFd/utils/helpers/__init__.py +++ b/CTFd/utils/helpers/__init__.py @@ -29,7 +29,12 @@ def get_errors(): @current_app.url_defaults def env_asset_url_default(endpoint, values): - """Create asset URLs dependent on the current env""" + """ + Create asset URLs dependent on the current env + + In CTFd 4.0 this url_for behavior and the themes_beta + route will be removed in favor of an improved theme system + """ if endpoint == "views.themes": path = values.get("path", "") static_asset = path.endswith(".js") or path.endswith(".css") diff --git a/CTFd/utils/initialization/__init__.py b/CTFd/utils/initialization/__init__.py index 3cc0351a..96664b92 100644 --- a/CTFd/utils/initialization/__init__.py +++ b/CTFd/utils/initialization/__init__.py @@ -53,6 +53,7 @@ def init_template_filters(app): def init_template_globals(app): from CTFd.constants import JINJA_ENUMS + from CTFd.constants.assets import Assets from CTFd.constants.config import Configs from CTFd.constants.plugins import Plugins from CTFd.constants.sessions import Session @@ -101,6 +102,7 @@ def init_template_globals(app): app.jinja_env.globals.update(get_current_user_attrs=get_current_user_attrs) app.jinja_env.globals.update(get_current_team_attrs=get_current_team_attrs) app.jinja_env.globals.update(get_ip=get_ip) + app.jinja_env.globals.update(Assets=Assets) app.jinja_env.globals.update(Configs=Configs) app.jinja_env.globals.update(Plugins=Plugins) app.jinja_env.globals.update(Session=Session) diff --git a/CTFd/views.py b/CTFd/views.py index dc6311dd..5ca48b70 100644 --- a/CTFd/views.py +++ b/CTFd/views.py @@ -484,3 +484,23 @@ def themes(theme, path): if os.path.isfile(cand_path): return send_file(cand_path) abort(404) + + +@views.route("/themes//static/") +def themes_beta(theme, path): + """ + This is a copy of the above themes route used to avoid + the current appending of .dev and .min for theme assets. + + In CTFd 4.0 this url_for behavior and this themes_beta + route will be removed. + """ + for cand_path in ( + safe_join(app.root_path, "themes", cand_theme, "static", path) + # The `theme` value passed in may not be the configured one, e.g. for + # admin pages, so we check that first + for cand_theme in (theme, *config.ctf_theme_candidates()) + ): + if os.path.isfile(cand_path): + return send_file(cand_path) + abort(404)