From 9264e9642831e7dfcab5af8258b353e9a207e655 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 8 Sep 2020 00:08:35 -0400 Subject: [PATCH] Mark 3.1.0 (#1634) # 3.1.0 / 2020-09-08 **General** - Loosen team password confirmation in team settings to also accept the team captain's password to make it easier to change the team password - Adds the ability to add custom user and team fields for registration/profile settings. - Improve Notifications pubsub events system to use a subscriber per server instead of a subscriber per browser. This should improve the reliability of CTFd at higher load and make it easier to deploy the Notifications system **Admin Panel** - Add a comments functionality for admins to discuss challenges, users, teams, pages - Adds a legal section in Configs where users can add a terms of service and privacy policy - Add a Custom Fields section in Configs where admins can add/edit custom user/team fields - Move user graphs into a modal for Admin Panel **API** - Add `/api/v1/comments` to manipulate and create comments **Themes** - Make scoreboard caching only cache the score table instead of the entire page. This is done by caching the specific template section. Refer to #1586, specifically the changes in `scoreboard.html`. - Add rel=noopener to external links to prevent tab napping attacks - Change the registration page to reference links to Terms of Service and Privacy Policy if specified in configuration **Miscellaneous** - Make team settings modal larger in the core theme - Update tests in Github Actions to properly test under MySQL and Postgres - Make gevent default in serve.py and add a `--disable-gevent` switch in serve.py - Add `tenacity` library for retrying logic - Add `pytest-sugar` for slightly prettier pytest output - Add a `listen()` method to `CTFd.utils.events.EventManager` and `CTFd.utils.events.RedisEventManager`. - This method should implement subscription for a CTFd worker to whatever underlying notification system there is. This should be implemented with gevent or a background thread. - The `subscribe()` method (which used to implement the functionality of the new `listen()` function) now only handles passing notifications from CTFd to the browser. This should also be implemented with gevent or a background thread. --- .github/workflows/lint.yml | 3 +- .github/workflows/mysql.yml | 9 +- .github/workflows/postgres.yml | 3 +- .github/workflows/sqlite.yml | 2 +- CHANGELOG.md | 36 ++ CTFd/__init__.py | 2 +- CTFd/api/__init__.py | 2 + CTFd/api/v1/comments.py | 159 +++++++ CTFd/api/v1/config.py | 89 +++- CTFd/auth.py | 34 +- CTFd/cache/__init__.py | 7 +- CTFd/constants/__init__.py | 2 + CTFd/constants/config.py | 16 + CTFd/constants/static.py | 14 + CTFd/forms/auth.py | 22 +- CTFd/forms/config.py | 22 +- CTFd/forms/self.py | 34 +- CTFd/forms/teams.py | 183 +++++++- CTFd/forms/users.py | 114 ++++- CTFd/models/__init__.py | 130 ++++++ CTFd/schemas/comments.py | 14 + CTFd/schemas/fields.py | 38 ++ CTFd/schemas/teams.py | 141 +++++- CTFd/schemas/users.py | 134 +++++- CTFd/scoreboard.py | 2 - CTFd/teams.py | 57 ++- .../js/components/comments/CommentBox.vue | 237 ++++++++++ .../js/components/configs/fields/Field.vue | 200 +++++++++ .../components/configs/fields/FieldList.vue | 82 ++++ .../themes/admin/assets/js/pages/challenge.js | 10 + CTFd/themes/admin/assets/js/pages/configs.js | 32 ++ CTFd/themes/admin/assets/js/pages/editor.js | 12 + CTFd/themes/admin/assets/js/pages/team.js | 65 ++- CTFd/themes/admin/assets/js/pages/user.js | 55 ++- CTFd/themes/admin/static/js/components.dev.js | 277 ++++++++++++ CTFd/themes/admin/static/js/components.min.js | 1 + CTFd/themes/admin/static/js/core.dev.js | 62 +-- CTFd/themes/admin/static/js/core.min.js | 217 +++++++++ .../admin/static/js/echarts.bundle.min.js | 2 +- CTFd/themes/admin/static/js/graphs.dev.js | 2 +- CTFd/themes/admin/static/js/graphs.min.js | 15 + CTFd/themes/admin/static/js/helpers.dev.js | 2 +- CTFd/themes/admin/static/js/helpers.min.js | 2 +- .../admin/static/js/pages/challenge.dev.js | 4 +- .../admin/static/js/pages/challenge.min.js | 2 +- .../admin/static/js/pages/challenges.dev.js | 2 +- .../admin/static/js/pages/challenges.min.js | 2 +- .../admin/static/js/pages/configs.dev.js | 4 +- .../admin/static/js/pages/configs.min.js | 2 +- .../admin/static/js/pages/editor.dev.js | 4 +- .../admin/static/js/pages/editor.min.js | 2 +- CTFd/themes/admin/static/js/pages/main.dev.js | 2 +- CTFd/themes/admin/static/js/pages/main.min.js | 2 +- .../static/js/pages/notifications.dev.js | 2 +- .../static/js/pages/notifications.min.js | 2 +- .../themes/admin/static/js/pages/pages.dev.js | 2 +- .../themes/admin/static/js/pages/pages.min.js | 2 +- .../themes/admin/static/js/pages/reset.dev.js | 2 +- .../themes/admin/static/js/pages/reset.min.js | 2 +- .../admin/static/js/pages/scoreboard.dev.js | 2 +- .../admin/static/js/pages/scoreboard.min.js | 2 +- .../admin/static/js/pages/statistics.dev.js | 2 +- .../admin/static/js/pages/statistics.min.js | 2 +- .../admin/static/js/pages/submissions.dev.js | 2 +- .../admin/static/js/pages/submissions.min.js | 2 +- CTFd/themes/admin/static/js/pages/team.dev.js | 4 +- CTFd/themes/admin/static/js/pages/team.min.js | 2 +- .../themes/admin/static/js/pages/teams.dev.js | 2 +- .../themes/admin/static/js/pages/teams.min.js | 2 +- CTFd/themes/admin/static/js/pages/user.dev.js | 4 +- CTFd/themes/admin/static/js/pages/user.min.js | 2 +- .../themes/admin/static/js/pages/users.dev.js | 2 +- .../themes/admin/static/js/pages/users.min.js | 2 +- .../admin/static/js/vendor.bundle.dev.js | 36 ++ .../admin/static/js/vendor.bundle.min.js | 22 +- CTFd/themes/admin/templates/base.html | 1 + .../admin/templates/challenges/challenge.html | 11 +- CTFd/themes/admin/templates/config.html | 10 + .../admin/templates/configs/fields.html | 41 ++ .../themes/admin/templates/configs/legal.html | 56 +++ CTFd/themes/admin/templates/editor.html | 15 + CTFd/themes/admin/templates/macros/forms.html | 46 ++ .../admin/templates/modals/teams/create.html | 21 +- .../admin/templates/modals/teams/edit.html | 6 +- .../templates/modals/teams/statistics.html | 23 + .../admin/templates/modals/users/create.html | 7 + .../admin/templates/modals/users/edit.html | 4 + .../templates/modals/users/statistics.html | 23 + CTFd/themes/admin/templates/teams/team.html | 63 +-- CTFd/themes/admin/templates/teams/teams.html | 2 +- CTFd/themes/admin/templates/users/user.html | 59 +-- CTFd/themes/admin/templates/users/users.html | 2 +- CTFd/themes/core/assets/js/graphs.js | 4 + CTFd/themes/core/assets/js/helpers.js | 55 +++ CTFd/themes/core/assets/js/pages/settings.js | 13 + .../core/assets/js/pages/teams/private.js | 30 +- CTFd/themes/core/assets/js/utils.js | 2 +- CTFd/themes/core/static/js/core.dev.js | 2 +- CTFd/themes/core/static/js/core.min.js | 146 ++++++ CTFd/themes/core/static/js/helpers.dev.js | 2 +- CTFd/themes/core/static/js/helpers.min.js | 2 +- .../core/static/js/pages/challenges.min.js | 2 +- CTFd/themes/core/static/js/pages/main.min.js | 2 +- .../core/static/js/pages/notifications.min.js | 2 +- .../core/static/js/pages/scoreboard.min.js | 2 +- .../core/static/js/pages/settings.dev.js | 2 +- .../core/static/js/pages/settings.min.js | 2 +- CTFd/themes/core/static/js/pages/setup.min.js | 2 +- CTFd/themes/core/static/js/pages/stats.dev.js | 2 +- CTFd/themes/core/static/js/pages/stats.min.js | 2 +- .../core/static/js/pages/teams/private.dev.js | 2 +- .../core/static/js/pages/teams/private.min.js | 2 +- CTFd/themes/core/templates/macros/forms.html | 46 ++ CTFd/themes/core/templates/register.html | 25 ++ CTFd/themes/core/templates/scoreboard.html | 2 + CTFd/themes/core/templates/settings.html | 5 + .../themes/core/templates/teams/new_team.html | 8 +- CTFd/themes/core/templates/teams/private.html | 49 ++- CTFd/themes/core/templates/teams/public.html | 7 +- CTFd/themes/core/templates/teams/teams.html | 2 +- CTFd/themes/core/templates/users/private.html | 8 +- CTFd/themes/core/templates/users/public.html | 8 +- CTFd/themes/core/templates/users/users.html | 2 +- CTFd/utils/config/pages.py | 4 +- CTFd/utils/events/__init__.py | 98 +++-- CTFd/utils/initialization/__init__.py | 13 + CTFd/views.py | 26 +- README.md | 2 +- development.txt | 1 + .../0366ba6575ca_add_table_for_comments.py | 47 ++ ...0014_add_fields_and_fieldentries_tables.py | 53 +++ package.json | 2 +- requirements.txt | 1 + serve.py | 14 +- tests/admin/test_fields.py | 94 ++++ tests/api/v1/test_comments.py | 108 +++++ tests/api/v1/test_fields.py | 416 ++++++++++++++++++ tests/api/v1/test_scoreboard.py | 14 +- tests/api/v1/test_teams.py | 30 +- tests/helpers.py | 48 ++ tests/teams/test_fields.py | 251 +++++++++++ tests/test_legal.py | 27 ++ tests/users/test_fields.py | 203 +++++++++ tests/utils/test_events.py | 103 ++++- webpack.config.js | 27 +- 145 files changed, 4716 insertions(+), 366 deletions(-) create mode 100644 CTFd/api/v1/comments.py create mode 100644 CTFd/constants/static.py create mode 100644 CTFd/schemas/comments.py create mode 100644 CTFd/schemas/fields.py create mode 100644 CTFd/themes/admin/assets/js/components/comments/CommentBox.vue create mode 100644 CTFd/themes/admin/assets/js/components/configs/fields/Field.vue create mode 100644 CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue create mode 100644 CTFd/themes/admin/static/js/components.dev.js create mode 100644 CTFd/themes/admin/static/js/components.min.js create mode 100644 CTFd/themes/admin/templates/configs/fields.html create mode 100644 CTFd/themes/admin/templates/configs/legal.html create mode 100644 CTFd/themes/admin/templates/macros/forms.html create mode 100644 CTFd/themes/admin/templates/modals/teams/statistics.html create mode 100644 CTFd/themes/admin/templates/modals/users/statistics.html create mode 100644 CTFd/themes/core/templates/macros/forms.html create mode 100644 migrations/versions/0366ba6575ca_add_table_for_comments.py create mode 100644 migrations/versions/75e8ab9a0014_add_fields_and_fieldentries_tables.py create mode 100644 tests/admin/test_fields.py create mode 100644 tests/api/v1/test_comments.py create mode 100644 tests/api/v1/test_fields.py create mode 100644 tests/teams/test_fields.py create mode 100644 tests/test_legal.py create mode 100644 tests/users/test_fields.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 158966afd..35477ba6d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,6 @@ jobs: strategy: matrix: python-version: ['3.6'] - TESTING_DATABASE_URL: ['sqlite://'] name: Linting steps: @@ -30,6 +29,8 @@ jobs: - name: Lint run: make lint + env: + TESTING_DATABASE_URL: 'sqlite://' - name: Lint Dockerfile uses: brpaz/hadolint-action@master diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index ddfb5db18..d1dfdf4fc 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -9,9 +9,12 @@ jobs: runs-on: ubuntu-latest services: mysql: - image: mysql + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: password ports: - - 3306:3306 + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 redis: image: redis ports: @@ -20,7 +23,6 @@ jobs: strategy: matrix: python-version: ['3.6'] - TESTING_DATABASE_URL: ['mysql+pymysql://root@localhost/ctfd'] name: Python ${{ matrix.python-version }} steps: @@ -43,6 +45,7 @@ jobs: env: AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + TESTING_DATABASE_URL: mysql+pymysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/ctfd - name: Codecov uses: codecov/codecov-action@v1.0.11 diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 91b62be85..faed71a97 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -15,6 +15,7 @@ jobs: env: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_DB: ctfd + POSTGRES_PASSWORD: password # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready @@ -29,7 +30,6 @@ jobs: strategy: matrix: python-version: ['3.6'] - TESTING_DATABASE_URL: ['postgres://postgres@localhost/ctfd'] name: Python ${{ matrix.python-version }} steps: @@ -52,6 +52,7 @@ jobs: env: AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + TESTING_DATABASE_URL: postgres://postgres:password@localhost:${{ job.services.postgres.ports[5432] }}/ctfd - name: Codecov uses: codecov/codecov-action@v1.0.11 diff --git a/.github/workflows/sqlite.yml b/.github/workflows/sqlite.yml index 66af2dad2..9c8aec60e 100644 --- a/.github/workflows/sqlite.yml +++ b/.github/workflows/sqlite.yml @@ -11,7 +11,6 @@ jobs: strategy: matrix: python-version: ['3.6'] - TESTING_DATABASE_URL: ['sqlite://'] name: Python ${{ matrix.python-version }} steps: @@ -35,6 +34,7 @@ jobs: env: AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + TESTING_DATABASE_URL: 'sqlite://' - name: Codecov uses: codecov/codecov-action@v1.0.11 diff --git a/CHANGELOG.md b/CHANGELOG.md index 68a3c8039..e5e485887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +# 3.1.0 / 2020-09-08 + +**General** + +- Loosen team password confirmation in team settings to also accept the team captain's password to make it easier to change the team password +- Adds the ability to add custom user and team fields for registration/profile settings. +- Improve Notifications pubsub events system to use a subscriber per server instead of a subscriber per browser. This should improve the reliability of CTFd at higher load and make it easier to deploy the Notifications system + +**Admin Panel** + +- Add a comments functionality for admins to discuss challenges, users, teams, pages +- Adds a legal section in Configs where users can add a terms of service and privacy policy +- Add a Custom Fields section in Configs where admins can add/edit custom user/team fields +- Move user graphs into a modal for Admin Panel + +**API** + +- Add `/api/v1/comments` to manipulate and create comments + +**Themes** + +- Make scoreboard caching only cache the score table instead of the entire page. This is done by caching the specific template section. Refer to #1586, specifically the changes in `scoreboard.html`. +- Add rel=noopener to external links to prevent tab napping attacks +- Change the registration page to reference links to Terms of Service and Privacy Policy if specified in configuration + +**Miscellaneous** + +- Make team settings modal larger in the core theme +- Update tests in Github Actions to properly test under MySQL and Postgres +- Make gevent default in serve.py and add a `--disable-gevent` switch in serve.py +- Add `tenacity` library for retrying logic +- Add `pytest-sugar` for slightly prettier pytest output +- Add a `listen()` method to `CTFd.utils.events.EventManager` and `CTFd.utils.events.RedisEventManager`. + - This method should implement subscription for a CTFd worker to whatever underlying notification system there is. This should be implemented with gevent or a background thread. + - The `subscribe()` method (which used to implement the functionality of the new `listen()` function) now only handles passing notifications from CTFd to the browser. This should also be implemented with gevent or a background thread. + # 3.0.2 / 2020-08-23 **Admin Panel** diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 5c9826b70..38f5a3af3 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -26,7 +26,7 @@ from CTFd.utils.sessions import CachingSessionInterface from CTFd.utils.updates import update_check -__version__ = "3.0.2" +__version__ = "3.1.0" __channel__ = "oss" diff --git a/CTFd/api/__init__.py b/CTFd/api/__init__.py index e53a0ac0f..3c70f839d 100644 --- a/CTFd/api/__init__.py +++ b/CTFd/api/__init__.py @@ -3,6 +3,7 @@ from CTFd.api.v1.awards import awards_namespace from CTFd.api.v1.challenges import challenges_namespace +from CTFd.api.v1.comments import comments_namespace from CTFd.api.v1.config import configs_namespace from CTFd.api.v1.files import files_namespace from CTFd.api.v1.flags import flags_namespace @@ -48,3 +49,4 @@ CTFd_API_v1.add_namespace(pages_namespace, "/pages") CTFd_API_v1.add_namespace(unlocks_namespace, "/unlocks") CTFd_API_v1.add_namespace(tokens_namespace, "/tokens") +CTFd_API_v1.add_namespace(comments_namespace, "/comments") diff --git a/CTFd/api/v1/comments.py b/CTFd/api/v1/comments.py new file mode 100644 index 000000000..4b3fac44e --- /dev/null +++ b/CTFd/api/v1/comments.py @@ -0,0 +1,159 @@ +from typing import List + +from flask import request, session +from flask_restx import Namespace, Resource + +from CTFd.api.v1.helpers.request import validate_args +from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic +from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse +from CTFd.constants import RawEnum +from CTFd.models import ( + ChallengeComments, + Comments, + PageComments, + TeamComments, + UserComments, + db, +) +from CTFd.schemas.comments import CommentSchema +from CTFd.utils.decorators import admins_only +from CTFd.utils.helpers.models import build_model_filters + +comments_namespace = Namespace("comments", description="Endpoint to retrieve Comments") + + +CommentModel = sqlalchemy_to_pydantic(Comments) + + +class CommentDetailedSuccessResponse(APIDetailedSuccessResponse): + data: CommentModel + + +class CommentListSuccessResponse(APIListSuccessResponse): + data: List[CommentModel] + + +comments_namespace.schema_model( + "CommentDetailedSuccessResponse", CommentDetailedSuccessResponse.apidoc() +) + +comments_namespace.schema_model( + "CommentListSuccessResponse", CommentListSuccessResponse.apidoc() +) + + +def get_comment_model(data): + model = Comments + if "challenge_id" in data: + model = ChallengeComments + elif "user_id" in data: + model = UserComments + elif "team_id" in data: + model = TeamComments + elif "page_id" in data: + model = PageComments + else: + model = Comments + return model + + +@comments_namespace.route("") +class CommentList(Resource): + @admins_only + @comments_namespace.doc( + description="Endpoint to list Comment objects in bulk", + responses={ + 200: ("Success", "CommentListSuccessResponse"), + 400: ( + "An error occured processing the provided or stored data", + "APISimpleErrorResponse", + ), + }, + ) + @validate_args( + { + "challenge_id": (int, None), + "user_id": (int, None), + "team_id": (int, None), + "page_id": (int, None), + "q": (str, None), + "field": (RawEnum("CommentFields", {"content": "content"}), None), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + CommentModel = get_comment_model(data=query_args) + filters = build_model_filters(model=CommentModel, query=q, field=field) + + comments = ( + CommentModel.query.filter_by(**query_args) + .filter(*filters) + .order_by(CommentModel.id.desc()) + .paginate(max_per_page=100) + ) + schema = CommentSchema(many=True) + response = schema.dump(comments.items) + + if response.errors: + return {"success": False, "errors": response.errors}, 400 + + return { + "meta": { + "pagination": { + "page": comments.page, + "next": comments.next_num, + "prev": comments.prev_num, + "pages": comments.pages, + "per_page": comments.per_page, + "total": comments.total, + } + }, + "success": True, + "data": response.data, + } + + @admins_only + @comments_namespace.doc( + description="Endpoint to create a Comment object", + responses={ + 200: ("Success", "CommentDetailedSuccessResponse"), + 400: ( + "An error occured processing the provided or stored data", + "APISimpleErrorResponse", + ), + }, + ) + def post(self): + req = request.get_json() + # Always force author IDs to be the actual user + req["author_id"] = session["id"] + CommentModel = get_comment_model(data=req) + + m = CommentModel(**req) + db.session.add(m) + db.session.commit() + + schema = CommentSchema() + + response = schema.dump(m) + db.session.close() + + return {"success": True, "data": response.data} + + +@comments_namespace.route("/") +class Comment(Resource): + @admins_only + @comments_namespace.doc( + description="Endpoint to delete a specific Comment object", + responses={200: ("Success", "APISimpleSuccessResponse")}, + ) + def delete(self, comment_id): + comment = Comments.query.filter_by(id=comment_id).first_or_404() + db.session.delete(comment) + db.session.commit() + db.session.close() + + return {"success": True} diff --git a/CTFd/api/v1/config.py b/CTFd/api/v1/config.py index f44729821..ae1c686e2 100644 --- a/CTFd/api/v1/config.py +++ b/CTFd/api/v1/config.py @@ -8,8 +8,9 @@ from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse from CTFd.cache import clear_config, clear_standings from CTFd.constants import RawEnum -from CTFd.models import Configs, db +from CTFd.models import Configs, Fields, db from CTFd.schemas.config import ConfigSchema +from CTFd.schemas.fields import FieldSchema from CTFd.utils import set_config from CTFd.utils.decorators import admins_only from CTFd.utils.helpers.models import build_model_filters @@ -189,3 +190,89 @@ def delete(self, config_key): clear_standings() return {"success": True} + + +@configs_namespace.route("/fields") +class FieldList(Resource): + @admins_only + @validate_args( + { + "type": (str, None), + "q": (str, None), + "field": (RawEnum("FieldFields", {"description": "description"}), None), + }, + location="query", + ) + def get(self, query_args): + q = query_args.pop("q", None) + field = str(query_args.pop("field", None)) + filters = build_model_filters(model=Fields, query=q, field=field) + + fields = Fields.query.filter_by(**query_args).filter(*filters).all() + schema = FieldSchema(many=True) + + response = schema.dump(fields) + + if response.errors: + return {"success": False, "errors": response.errors}, 400 + + return {"success": True, "data": response.data} + + @admins_only + def post(self): + req = request.get_json() + schema = FieldSchema() + response = schema.load(req, session=db.session) + + if response.errors: + return {"success": False, "errors": response.errors}, 400 + + db.session.add(response.data) + db.session.commit() + + response = schema.dump(response.data) + db.session.close() + + return {"success": True, "data": response.data} + + +@configs_namespace.route("/fields/") +class Field(Resource): + @admins_only + def get(self, field_id): + field = Fields.query.filter_by(id=field_id).first_or_404() + schema = FieldSchema() + + response = schema.dump(field) + + if response.errors: + return {"success": False, "errors": response.errors}, 400 + + return {"success": True, "data": response.data} + + @admins_only + def patch(self, field_id): + field = Fields.query.filter_by(id=field_id).first_or_404() + schema = FieldSchema() + + req = request.get_json() + + response = schema.load(req, session=db.session, instance=field) + if response.errors: + return {"success": False, "errors": response.errors}, 400 + + db.session.commit() + + response = schema.dump(response.data) + db.session.close() + + return {"success": True, "data": response.data} + + @admins_only + def delete(self, field_id): + field = Fields.query.filter_by(id=field_id).first_or_404() + db.session.delete(field) + db.session.commit() + db.session.close() + + return {"success": True} diff --git a/CTFd/auth.py b/CTFd/auth.py index f4ec1da4c..2f801f03f 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -7,7 +7,7 @@ from itsdangerous.exc import BadSignature, BadTimeSignature, SignatureExpired from CTFd.cache import clear_team_session, clear_user_session -from CTFd.models import Teams, Users, db +from CTFd.models import Teams, UserFieldEntries, UserFields, Users, db from CTFd.utils import config, email, get_app_config, get_config from CTFd.utils import user as current_user from CTFd.utils import validators @@ -206,6 +206,31 @@ def register(): valid_email = validators.validate_email(email_address) team_name_email_check = validators.validate_email(name) + # Process additional user fields + fields = {} + for field in UserFields.query.all(): + fields[field.id] = field + + entries = {} + for field_id, field in fields.items(): + value = request.form.get(f"fields[{field_id}]", "").strip() + if field.required is True and (value is None or value == ""): + errors.append("Please provide all required fields") + break + + # Handle special casing of existing profile fields + if field.name.lower() == "affiliation": + affiliation = value + break + elif field.name.lower() == "website": + website = value + break + + if field.field_type == "boolean": + entries[field_id] = bool(value) + else: + entries[field_id] = value + if country: try: validators.validate_country_code(country) @@ -275,6 +300,13 @@ def register(): db.session.commit() db.session.flush() + for field_id, value in entries.items(): + entry = UserFieldEntries( + field_id=field_id, value=value, user_id=user.id + ) + db.session.add(entry) + db.session.commit() + login_user(user) if config.can_send_mail() and get_config( diff --git a/CTFd/cache/__init__.py b/CTFd/cache/__init__.py index a7f92d974..d452712f6 100644 --- a/CTFd/cache/__init__.py +++ b/CTFd/cache/__init__.py @@ -1,5 +1,5 @@ from flask import request -from flask_caching import Cache +from flask_caching import Cache, make_template_fragment_key cache = Cache() @@ -27,6 +27,7 @@ def clear_config(): def clear_standings(): from CTFd.models import Users, Teams + from CTFd.constants.static import CacheKeys from CTFd.utils.scores import get_standings, get_team_standings, get_user_standings from CTFd.api.v1.scoreboard import ScoreboardDetail, ScoreboardList from CTFd.api import api @@ -55,11 +56,13 @@ def clear_standings(): cache.delete_memoized(get_team_place) # Clear out HTTP request responses - cache.delete(make_cache_key(path="scoreboard.listing")) cache.delete(make_cache_key(path=api.name + "." + ScoreboardList.endpoint)) cache.delete(make_cache_key(path=api.name + "." + ScoreboardDetail.endpoint)) cache.delete_memoized(ScoreboardList.get) + # Clear out scoreboard templates + cache.delete(make_template_fragment_key(CacheKeys.PUBLIC_SCOREBOARD_TABLE)) + def clear_pages(): from CTFd.utils.config.pages import get_page, get_pages diff --git a/CTFd/constants/__init__.py b/CTFd/constants/__init__.py index fc9a1baf1..72b09b35e 100644 --- a/CTFd/constants/__init__.py +++ b/CTFd/constants/__init__.py @@ -3,6 +3,7 @@ from flask import current_app JS_ENUMS = {} +JINJA_ENUMS = {} class RawEnum(Enum): @@ -59,6 +60,7 @@ def JinjaEnum(cls): """ if cls.__name__ not in current_app.jinja_env.globals: current_app.jinja_env.globals[cls.__name__] = cls + JINJA_ENUMS[cls.__name__] = cls else: raise KeyError("{} was already defined as a JinjaEnum".format(cls.__name__)) return cls diff --git a/CTFd/constants/config.py b/CTFd/constants/config.py index 0c65bb7ba..ed9ed7fcf 100644 --- a/CTFd/constants/config.py +++ b/CTFd/constants/config.py @@ -1,5 +1,7 @@ import json +from flask import url_for + from CTFd.constants import JinjaEnum, RawEnum from CTFd.utils import get_config @@ -63,5 +65,19 @@ def theme_footer(self): def theme_settings(self): return json.loads(get_config("theme_settings", default="null")) + @property + def tos_or_privacy(self): + tos = bool(get_config("tos_url") or get_config("tos_text")) + privacy = bool(get_config("privacy_url") or get_config("privacy_text")) + return tos or privacy + + @property + def tos_link(self): + return get_config("tos_url", default=url_for("views.tos")) + + @property + def privacy_link(self): + return get_config("privacy_url", default=url_for("views.privacy")) + Configs = _ConfigsWrapper() diff --git a/CTFd/constants/static.py b/CTFd/constants/static.py new file mode 100644 index 000000000..93a380f78 --- /dev/null +++ b/CTFd/constants/static.py @@ -0,0 +1,14 @@ +from CTFd.constants import JinjaEnum, RawEnum + + +@JinjaEnum +class CacheKeys(str, RawEnum): + PUBLIC_SCOREBOARD_TABLE = "public_scoreboard_table" + + +# Placeholder object. Not used, just imported to force initialization of any Enums here +class _StaticsWrapper: + pass + + +Static = _StaticsWrapper() diff --git a/CTFd/forms/auth.py b/CTFd/forms/auth.py index 7708b5617..b0c70402a 100644 --- a/CTFd/forms/auth.py +++ b/CTFd/forms/auth.py @@ -4,13 +4,25 @@ from CTFd.forms import BaseForm from CTFd.forms.fields import SubmitField +from CTFd.forms.users import attach_custom_user_fields, build_custom_user_fields -class RegistrationForm(BaseForm): - name = StringField("User Name", validators=[InputRequired()]) - email = EmailField("Email", validators=[InputRequired()]) - password = PasswordField("Password", validators=[InputRequired()]) - submit = SubmitField("Submit") +def RegistrationForm(*args, **kwargs): + class _RegistrationForm(BaseForm): + name = StringField("User Name", validators=[InputRequired()]) + email = EmailField("Email", validators=[InputRequired()]) + password = PasswordField("Password", validators=[InputRequired()]) + submit = SubmitField("Submit") + + @property + def extra(self): + return build_custom_user_fields( + self, include_entries=False, blacklisted_items=() + ) + + attach_custom_user_fields(_RegistrationForm) + + return _RegistrationForm(*args, **kwargs) class LoginForm(BaseForm): diff --git a/CTFd/forms/config.py b/CTFd/forms/config.py index 301adea09..6c8cb6e98 100644 --- a/CTFd/forms/config.py +++ b/CTFd/forms/config.py @@ -1,5 +1,5 @@ -from wtforms import BooleanField, SelectField, StringField -from wtforms.fields.html5 import IntegerField +from wtforms import BooleanField, SelectField, StringField, TextAreaField +from wtforms.fields.html5 import IntegerField, URLField from wtforms.widgets.html5 import NumberInput from CTFd.forms import BaseForm @@ -60,3 +60,21 @@ class ExportCSVForm(BaseForm): ), ) submit = SubmitField("Download CSV") + + +class LegalSettingsForm(BaseForm): + tos_url = URLField( + "Terms of Service URL", + description="External URL to a Terms of Service document hosted elsewhere", + ) + tos_text = TextAreaField( + "Terms of Service", description="Text shown on the Terms of Service page", + ) + privacy_url = URLField( + "Privacy Policy URL", + description="External URL to a Privacy Policy document hosted elsewhere", + ) + privacy_text = TextAreaField( + "Privacy Policy", description="Text shown on the Privacy Policy page", + ) + submit = SubmitField("Update") diff --git a/CTFd/forms/self.py b/CTFd/forms/self.py index a57b2af22..13a32035b 100644 --- a/CTFd/forms/self.py +++ b/CTFd/forms/self.py @@ -1,20 +1,36 @@ +from flask import session from wtforms import PasswordField, SelectField, StringField from wtforms.fields.html5 import DateField, URLField from CTFd.forms import BaseForm from CTFd.forms.fields import SubmitField +from CTFd.forms.users import attach_custom_user_fields, build_custom_user_fields from CTFd.utils.countries import SELECT_COUNTRIES_LIST -class SettingsForm(BaseForm): - name = StringField("User Name") - email = StringField("Email") - password = PasswordField("Password") - confirm = PasswordField("Current Password") - affiliation = StringField("Affiliation") - website = URLField("Website") - country = SelectField("Country", choices=SELECT_COUNTRIES_LIST) - submit = SubmitField("Submit") +def SettingsForm(*args, **kwargs): + class _SettingsForm(BaseForm): + name = StringField("User Name") + email = StringField("Email") + password = PasswordField("Password") + confirm = PasswordField("Current Password") + affiliation = StringField("Affiliation") + website = URLField("Website") + country = SelectField("Country", choices=SELECT_COUNTRIES_LIST) + submit = SubmitField("Submit") + + @property + def extra(self): + return build_custom_user_fields( + self, + include_entries=True, + fields_kwargs={"editable": True}, + field_entries_kwargs={"user_id": session["id"]}, + ) + + attach_custom_user_fields(_SettingsForm, editable=True) + + return _SettingsForm(*args, **kwargs) class TokensForm(BaseForm): diff --git a/CTFd/forms/teams.py b/CTFd/forms/teams.py index fc047cad5..80ccb27e5 100644 --- a/CTFd/forms/teams.py +++ b/CTFd/forms/teams.py @@ -4,29 +4,142 @@ from CTFd.forms import BaseForm from CTFd.forms.fields import SubmitField +from CTFd.models import TeamFieldEntries, TeamFields from CTFd.utils.countries import SELECT_COUNTRIES_LIST -class TeamJoinForm(BaseForm): - name = StringField("Team Name", validators=[InputRequired()]) - password = PasswordField("Team Password", validators=[InputRequired()]) - submit = SubmitField("Join") +def build_custom_team_fields( + form_cls, + include_entries=False, + fields_kwargs=None, + field_entries_kwargs=None, + blacklisted_items=("affiliation", "website"), +): + if fields_kwargs is None: + fields_kwargs = {} + if field_entries_kwargs is None: + field_entries_kwargs = {} + + fields = [] + new_fields = TeamFields.query.filter_by(**fields_kwargs).all() + user_fields = {} + + # Only include preexisting values if asked + if include_entries is True: + for f in TeamFieldEntries.query.filter_by(**field_entries_kwargs).all(): + user_fields[f.field_id] = f.value + + for field in new_fields: + if field.name.lower() in blacklisted_items: + continue + + form_field = getattr(form_cls, f"fields[{field.id}]") + + # Add the field_type to the field so we know how to render it + form_field.field_type = field.field_type + + # Only include preexisting values if asked + if include_entries is True: + initial = user_fields.get(field.id, "") + form_field.data = initial + if form_field.render_kw: + form_field.render_kw["data-initial"] = initial + else: + form_field.render_kw = {"data-initial": initial} + + fields.append(form_field) + return fields + + +def attach_custom_team_fields(form_cls, **kwargs): + new_fields = TeamFields.query.filter_by(**kwargs).all() + for field in new_fields: + validators = [] + if field.required: + validators.append(InputRequired()) + + if field.field_type == "text": + input_field = StringField( + field.name, description=field.description, validators=validators + ) + elif field.field_type == "boolean": + input_field = BooleanField( + field.name, description=field.description, validators=validators + ) + + setattr(form_cls, f"fields[{field.id}]", input_field) -class TeamRegisterForm(BaseForm): +class TeamJoinForm(BaseForm): name = StringField("Team Name", validators=[InputRequired()]) password = PasswordField("Team Password", validators=[InputRequired()]) - submit = SubmitField("Create") + submit = SubmitField("Join") -class TeamSettingsForm(BaseForm): - name = StringField("Team Name") - confirm = PasswordField("Current Password") - password = PasswordField("Team Password") - affiliation = StringField("Affiliation") - website = URLField("Website") - country = SelectField("Country", choices=SELECT_COUNTRIES_LIST) - submit = SubmitField("Submit") +def TeamRegisterForm(*args, **kwargs): + class _TeamRegisterForm(BaseForm): + name = StringField("Team Name", validators=[InputRequired()]) + password = PasswordField("Team Password", validators=[InputRequired()]) + submit = SubmitField("Create") + + @property + def extra(self): + return build_custom_team_fields( + self, include_entries=False, blacklisted_items=() + ) + + attach_custom_team_fields(_TeamRegisterForm) + return _TeamRegisterForm(*args, **kwargs) + + +def TeamSettingsForm(*args, **kwargs): + class _TeamSettingsForm(BaseForm): + name = StringField( + "Team Name", + description="Your team's public name shown to other competitors", + ) + password = PasswordField( + "New Team Password", description="Set a new team join password" + ) + confirm = PasswordField( + "Confirm Password", + description="Provide your current team password (or your password) to update your team's password", + ) + affiliation = StringField( + "Affiliation", + description="Your team's affiliation publicly shown to other competitors", + ) + website = URLField( + "Website", + description="Your team's website publicly shown to other competitors", + ) + country = SelectField( + "Country", + choices=SELECT_COUNTRIES_LIST, + description="Your team's country publicly shown to other competitors", + ) + submit = SubmitField("Submit") + + @property + def extra(self): + return build_custom_team_fields( + self, + include_entries=True, + fields_kwargs={"editable": True}, + field_entries_kwargs={"team_id": self.obj.id}, + ) + + def __init__(self, *args, **kwargs): + """ + Custom init to persist the obj parameter to the rest of the form + """ + super().__init__(*args, **kwargs) + obj = kwargs.get("obj") + if obj: + self.obj = obj + + attach_custom_team_fields(_TeamSettingsForm) + return _TeamSettingsForm(*args, **kwargs) class TeamCaptainForm(BaseForm): @@ -66,7 +179,7 @@ class PublicTeamSearchForm(BaseForm): submit = SubmitField("Search") -class TeamCreateForm(BaseForm): +class TeamBaseForm(BaseForm): name = StringField("Team Name", validators=[InputRequired()]) email = EmailField("Email") password = PasswordField("Password") @@ -78,5 +191,41 @@ class TeamCreateForm(BaseForm): submit = SubmitField("Submit") -class TeamEditForm(TeamCreateForm): - pass +def TeamCreateForm(*args, **kwargs): + class _TeamCreateForm(TeamBaseForm): + pass + + @property + def extra(self): + return build_custom_team_fields(self, include_entries=False) + + attach_custom_team_fields(_TeamCreateForm) + + return _TeamCreateForm(*args, **kwargs) + + +def TeamEditForm(*args, **kwargs): + class _TeamEditForm(TeamBaseForm): + pass + + @property + def extra(self): + return build_custom_team_fields( + self, + include_entries=True, + fields_kwargs=None, + field_entries_kwargs={"team_id": self.obj.id}, + ) + + def __init__(self, *args, **kwargs): + """ + Custom init to persist the obj parameter to the rest of the form + """ + super().__init__(*args, **kwargs) + obj = kwargs.get("obj") + if obj: + self.obj = obj + + attach_custom_team_fields(_TeamEditForm) + + return _TeamEditForm(*args, **kwargs) diff --git a/CTFd/forms/users.py b/CTFd/forms/users.py index 7b47d1726..4962aa22b 100644 --- a/CTFd/forms/users.py +++ b/CTFd/forms/users.py @@ -4,9 +4,81 @@ from CTFd.forms import BaseForm from CTFd.forms.fields import SubmitField +from CTFd.models import UserFieldEntries, UserFields from CTFd.utils.countries import SELECT_COUNTRIES_LIST +def build_custom_user_fields( + form_cls, + include_entries=False, + fields_kwargs=None, + field_entries_kwargs=None, + blacklisted_items=("affiliation", "website"), +): + """ + Function used to reinject values back into forms for accessing by themes + """ + if fields_kwargs is None: + fields_kwargs = {} + if field_entries_kwargs is None: + field_entries_kwargs = {} + + fields = [] + new_fields = UserFields.query.filter_by(**fields_kwargs).all() + user_fields = {} + + # Only include preexisting values if asked + if include_entries is True: + for f in UserFieldEntries.query.filter_by(**field_entries_kwargs).all(): + user_fields[f.field_id] = f.value + + for field in new_fields: + if field.name.lower() in blacklisted_items: + continue + + form_field = getattr(form_cls, f"fields[{field.id}]") + + # Add the field_type to the field so we know how to render it + form_field.field_type = field.field_type + + # Only include preexisting values if asked + if include_entries is True: + initial = user_fields.get(field.id, "") + form_field.data = initial + if form_field.render_kw: + form_field.render_kw["data-initial"] = initial + else: + form_field.render_kw = {"data-initial": initial} + + fields.append(form_field) + return fields + + +def attach_custom_user_fields(form_cls, **kwargs): + """ + Function used to attach form fields to wtforms. + Not really a great solution but is approved by wtforms. + + https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition + """ + new_fields = UserFields.query.filter_by(**kwargs).all() + for field in new_fields: + validators = [] + if field.required: + validators.append(InputRequired()) + + if field.field_type == "text": + input_field = StringField( + field.name, description=field.description, validators=validators + ) + elif field.field_type == "boolean": + input_field = BooleanField( + field.name, description=field.description, validators=validators + ) + + setattr(form_cls, f"fields[{field.id}]", input_field) + + class UserSearchForm(BaseForm): field = SelectField( "Search Field", @@ -40,7 +112,7 @@ class PublicUserSearchForm(BaseForm): submit = SubmitField("Search") -class UserEditForm(BaseForm): +class UserBaseForm(BaseForm): name = StringField("User Name", validators=[InputRequired()]) email = EmailField("Email", validators=[InputRequired()]) password = PasswordField("Password") @@ -54,5 +126,41 @@ class UserEditForm(BaseForm): submit = SubmitField("Submit") -class UserCreateForm(UserEditForm): - notify = BooleanField("Email account credentials to user", default=True) +def UserEditForm(*args, **kwargs): + class _UserEditForm(UserBaseForm): + pass + + @property + def extra(self): + return build_custom_user_fields( + self, + include_entries=True, + fields_kwargs=None, + field_entries_kwargs={"user_id": self.obj.id}, + ) + + def __init__(self, *args, **kwargs): + """ + Custom init to persist the obj parameter to the rest of the form + """ + super().__init__(*args, **kwargs) + obj = kwargs.get("obj") + if obj: + self.obj = obj + + attach_custom_user_fields(_UserEditForm) + + return _UserEditForm(*args, **kwargs) + + +def UserCreateForm(*args, **kwargs): + class _UserCreateForm(UserBaseForm): + notify = BooleanField("Email account credentials to user", default=True) + + @property + def extra(self): + return build_custom_user_fields(self, include_entries=False) + + attach_custom_user_fields(_UserCreateForm) + + return _UserCreateForm(*args, **kwargs) diff --git a/CTFd/models/__init__.py b/CTFd/models/__init__.py index b58aa7e0d..079492346 100644 --- a/CTFd/models/__init__.py +++ b/CTFd/models/__init__.py @@ -77,6 +77,7 @@ class Challenges(db.Model): tags = db.relationship("Tags", backref="challenge") hints = db.relationship("Hints", backref="challenge") flags = db.relationship("Flags", backref="challenge") + comments = db.relationship("ChallengeComments", backref="challenge") class alt_defaultdict(defaultdict): """ @@ -275,6 +276,10 @@ class Users(db.Model): # Relationship for Teams team_id = db.Column(db.Integer, db.ForeignKey("teams.id")) + field_entries = db.relationship( + "UserFieldEntries", foreign_keys="UserFieldEntries.user_id", lazy="joined" + ) + created = db.Column(db.DateTime, default=datetime.datetime.utcnow) __mapper_args__ = {"polymorphic_identity": "user", "polymorphic_on": type} @@ -308,6 +313,10 @@ def account(self): elif user_mode == "users": return self + @property + def fields(self): + return self.get_fields(admin=False) + @property def solves(self): return self.get_solves(admin=False) @@ -333,6 +342,14 @@ def place(self): else: return None + def get_fields(self, admin=False): + if admin: + return self.field_entries + + return [ + entry for entry in self.field_entries if entry.field.public and entry.value + ] + def get_solves(self, admin=False): from CTFd.utils import get_config @@ -452,6 +469,10 @@ class Teams(db.Model): captain_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL")) captain = db.relationship("Users", foreign_keys=[captain_id]) + field_entries = db.relationship( + "TeamFieldEntries", foreign_keys="TeamFieldEntries.team_id", lazy="joined" + ) + created = db.Column(db.DateTime, default=datetime.datetime.utcnow) def __init__(self, **kwargs): @@ -463,6 +484,10 @@ def validate_password(self, key, plaintext): return hash_password(str(plaintext)) + @property + def fields(self): + return self.get_fields(admin=False) + @property def solves(self): return self.get_solves(admin=False) @@ -488,6 +513,14 @@ def place(self): else: return None + def get_fields(self, admin=False): + if admin: + return self.field_entries + + return [ + entry for entry in self.field_entries if entry.field.public and entry.value + ] + def get_solves(self, admin=False): from CTFd.utils import get_config @@ -739,3 +772,100 @@ def __repr__(self): class UserTokens(Tokens): __mapper_args__ = {"polymorphic_identity": "user"} + + +class Comments(db.Model): + __tablename__ = "comments" + id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String(80), default="standard") + content = db.Column(db.Text) + date = db.Column(db.DateTime, default=datetime.datetime.utcnow) + author_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")) + author = db.relationship("Users", foreign_keys="Comments.author_id", lazy="select") + + @property + def html(self): + from CTFd.utils.config.pages import build_html + from CTFd.utils.helpers import markup + + return markup(build_html(self.content, sanitize=True)) + + __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type} + + +class ChallengeComments(Comments): + __mapper_args__ = {"polymorphic_identity": "challenge"} + challenge_id = db.Column( + db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE") + ) + + +class UserComments(Comments): + __mapper_args__ = {"polymorphic_identity": "user"} + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")) + + +class TeamComments(Comments): + __mapper_args__ = {"polymorphic_identity": "team"} + team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE")) + + +class PageComments(Comments): + __mapper_args__ = {"polymorphic_identity": "page"} + page_id = db.Column(db.Integer, db.ForeignKey("pages.id", ondelete="CASCADE")) + + +class Fields(db.Model): + __tablename__ = "fields" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.Text) + type = db.Column(db.String(80), default="standard") + field_type = db.Column(db.String(80)) + description = db.Column(db.Text) + required = db.Column(db.Boolean, default=False) + public = db.Column(db.Boolean, default=False) + editable = db.Column(db.Boolean, default=False) + + __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type} + + +class UserFields(Fields): + __mapper_args__ = {"polymorphic_identity": "user"} + + +class TeamFields(Fields): + __mapper_args__ = {"polymorphic_identity": "team"} + + +class FieldEntries(db.Model): + __tablename__ = "field_entries" + id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String(80), default="standard") + value = db.Column(db.JSON) + field_id = db.Column(db.Integer, db.ForeignKey("fields.id", ondelete="CASCADE")) + + field = db.relationship( + "Fields", foreign_keys="FieldEntries.field_id", lazy="joined" + ) + + __mapper_args__ = {"polymorphic_identity": "standard", "polymorphic_on": type} + + @hybrid_property + def name(self): + return self.field.name + + @hybrid_property + def description(self): + return self.field.description + + +class UserFieldEntries(FieldEntries): + __mapper_args__ = {"polymorphic_identity": "user"} + user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE")) + user = db.relationship("Users", foreign_keys="UserFieldEntries.user_id") + + +class TeamFieldEntries(FieldEntries): + __mapper_args__ = {"polymorphic_identity": "team"} + team_id = db.Column(db.Integer, db.ForeignKey("teams.id", ondelete="CASCADE")) + team = db.relationship("Teams", foreign_keys="TeamFieldEntries.team_id") diff --git a/CTFd/schemas/comments.py b/CTFd/schemas/comments.py new file mode 100644 index 000000000..d5ef43bf2 --- /dev/null +++ b/CTFd/schemas/comments.py @@ -0,0 +1,14 @@ +from marshmallow import fields + +from CTFd.models import Comments, ma +from CTFd.schemas.users import UserSchema + + +class CommentSchema(ma.ModelSchema): + class Meta: + model = Comments + include_fk = True + dump_only = ("id", "date", "html", "author", "author_id", "type") + + author = fields.Nested(UserSchema(only=("name",))) + html = fields.String() diff --git a/CTFd/schemas/fields.py b/CTFd/schemas/fields.py new file mode 100644 index 000000000..0199b9d7f --- /dev/null +++ b/CTFd/schemas/fields.py @@ -0,0 +1,38 @@ +from marshmallow import fields + +from CTFd.models import Fields, TeamFieldEntries, UserFieldEntries, db, ma + + +class FieldSchema(ma.ModelSchema): + class Meta: + model = Fields + include_fk = True + dump_only = ("id",) + + +class UserFieldEntriesSchema(ma.ModelSchema): + class Meta: + model = UserFieldEntries + sqla_session = db.session + include_fk = True + load_only = ("id",) + exclude = ("field", "user", "user_id") + dump_only = ("user_id", "name", "description", "type") + + name = fields.Nested(FieldSchema, only=("name"), attribute="field") + description = fields.Nested(FieldSchema, only=("description"), attribute="field") + type = fields.Nested(FieldSchema, only=("field_type"), attribute="field") + + +class TeamFieldEntriesSchema(ma.ModelSchema): + class Meta: + model = TeamFieldEntries + sqla_session = db.session + include_fk = True + load_only = ("id",) + exclude = ("field", "team", "team_id") + dump_only = ("team_id", "name", "description", "type") + + name = fields.Nested(FieldSchema, only=("name"), attribute="field") + description = fields.Nested(FieldSchema, only=("description"), attribute="field") + type = fields.Nested(FieldSchema, only=("field_type"), attribute="field") diff --git a/CTFd/schemas/teams.py b/CTFd/schemas/teams.py index be02c5fdb..68639c629 100644 --- a/CTFd/schemas/teams.py +++ b/CTFd/schemas/teams.py @@ -1,7 +1,10 @@ -from marshmallow import ValidationError, pre_load, validate +from marshmallow import ValidationError, post_dump, pre_load, validate +from marshmallow.fields import Nested from marshmallow_sqlalchemy import field_for +from sqlalchemy.orm import load_only -from CTFd.models import Teams, Users, ma +from CTFd.models import TeamFieldEntries, TeamFields, Teams, Users, ma +from CTFd.schemas.fields import TeamFieldEntriesSchema from CTFd.utils import get_config, string_types from CTFd.utils.crypto import verify_password from CTFd.utils.user import get_current_team, get_current_user, is_admin @@ -44,6 +47,9 @@ class Meta: ], ) country = field_for(Teams, "country", validate=[validate_country_code]) + fields = Nested( + TeamFieldEntriesSchema, partial=True, many=True, attribute="field_entries" + ) @pre_load def validate_name(self, data): @@ -142,10 +148,13 @@ def validate_password_confirmation(self, data): ) if password and confirm: - test = verify_password( + test_team = verify_password( plaintext=confirm, ciphertext=current_team.password ) - if test is True: + test_captain = verify_password( + plaintext=confirm, ciphertext=current_user.password + ) + if test_team is True or test_captain is True: return data else: raise ValidationError( @@ -183,6 +192,126 @@ def validate_captain_id(self, data): field_names=["captain_id"], ) + @pre_load + def validate_fields(self, data): + """ + This validator is used to only allow users to update the field entry for their user. + It's not possible to exclude it because without the PK Marshmallow cannot load the right instance + """ + fields = data.get("fields") + if fields is None: + return + + current_team = get_current_team() + + if is_admin(): + team_id = data.get("id") + if team_id: + target_team = Teams.query.filter_by(id=data["id"]).first() + else: + target_team = current_team + + # We are editting an existing + if self.view == "admin" and self.instance: + target_team = self.instance + provided_ids = [] + for f in fields: + f.pop("id", None) + field_id = f.get("field_id") + + # # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce + field = TeamFields.query.filter_by(id=field_id).first_or_404() + + # Get the existing field entry if one exists + entry = TeamFieldEntries.query.filter_by( + field_id=field.id, team_id=target_team.id + ).first() + if entry: + f["id"] = entry.id + provided_ids.append(entry.id) + + # Extremely dirty hack to prevent deleting previously provided data. + # This needs a better soln. + entries = ( + TeamFieldEntries.query.options(load_only("id")) + .filter_by(team_id=target_team.id) + .all() + ) + for entry in entries: + if entry.id not in provided_ids: + fields.append({"id": entry.id}) + else: + provided_ids = [] + for f in fields: + # Remove any existing set + f.pop("id", None) + field_id = f.get("field_id") + value = f.get("value") + + # # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce + field = TeamFields.query.filter_by(id=field_id).first_or_404() + + if field.required is True and value.strip() == "": + raise ValidationError( + f"Field '{field.name}' is required", field_names=["fields"] + ) + + if field.editable is False: + raise ValidationError( + f"Field '{field.name}' cannot be editted", + field_names=["fields"], + ) + + # Get the existing field entry if one exists + entry = TeamFieldEntries.query.filter_by( + field_id=field.id, team_id=current_team.id + ).first() + + if entry: + f["id"] = entry.id + provided_ids.append(entry.id) + + # Extremely dirty hack to prevent deleting previously provided data. + # This needs a better soln. + entries = ( + TeamFieldEntries.query.options(load_only("id")) + .filter_by(team_id=current_team.id) + .all() + ) + for entry in entries: + if entry.id not in provided_ids: + fields.append({"id": entry.id}) + + @post_dump + def process_fields(self, data): + """ + Handle permissions levels for fields. + This is post_dump to manipulate JSON instead of the raw db object + + Admins can see all fields. + Users (self) can see their edittable and public fields + Public (user) can only see public fields + """ + # Gather all possible fields + removed_field_ids = [] + fields = TeamFields.query.all() + + # Select fields for removal based on current view and properties of the field + for field in fields: + if self.view == "user": + if field.public is False: + removed_field_ids.append(field.id) + elif self.view == "self": + if field.editable is False and field.public is False: + removed_field_ids.append(field.id) + + # Rebuild fuilds + fields = data.get("fields") + if fields: + data["fields"] = [ + field for field in fields if field["field_id"] not in removed_field_ids + ] + views = { "user": [ "website", @@ -194,6 +323,7 @@ def validate_captain_id(self, data): "id", "oauth_id", "captain_id", + "fields", ], "self": [ "website", @@ -207,6 +337,7 @@ def validate_captain_id(self, data): "oauth_id", "password", "captain_id", + "fields", ], "admin": [ "website", @@ -224,6 +355,7 @@ def validate_captain_id(self, data): "oauth_id", "password", "captain_id", + "fields", ], } @@ -233,5 +365,6 @@ def __init__(self, view=None, *args, **kwargs): kwargs["only"] = self.views[view] elif isinstance(view, list): kwargs["only"] = view + self.view = view super(TeamSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/schemas/users.py b/CTFd/schemas/users.py index 071482557..66393061d 100644 --- a/CTFd/schemas/users.py +++ b/CTFd/schemas/users.py @@ -1,7 +1,10 @@ -from marshmallow import ValidationError, pre_load, validate +from marshmallow import ValidationError, post_dump, pre_load, validate +from marshmallow.fields import Nested from marshmallow_sqlalchemy import field_for +from sqlalchemy.orm import load_only -from CTFd.models import Users, ma +from CTFd.models import UserFieldEntries, UserFields, Users, ma +from CTFd.schemas.fields import UserFieldEntriesSchema from CTFd.utils import get_config, string_types from CTFd.utils.crypto import verify_password from CTFd.utils.email import check_email_is_whitelisted @@ -49,6 +52,9 @@ class Meta: ) country = field_for(Users, "country", validate=[validate_country_code]) password = field_for(Users, "password") + fields = Nested( + UserFieldEntriesSchema, partial=True, many=True, attribute="field_entries" + ) @pre_load def validate_name(self, data): @@ -180,6 +186,126 @@ def validate_password_confirmation(self, data): data.pop("password", None) data.pop("confirm", None) + @pre_load + def validate_fields(self, data): + """ + This validator is used to only allow users to update the field entry for their user. + It's not possible to exclude it because without the PK Marshmallow cannot load the right instance + """ + fields = data.get("fields") + if fields is None: + return + + current_user = get_current_user() + + if is_admin(): + user_id = data.get("id") + if user_id: + target_user = Users.query.filter_by(id=data["id"]).first() + else: + target_user = current_user + + # We are editting an existing user + if self.view == "admin" and self.instance: + target_user = self.instance + provided_ids = [] + for f in fields: + f.pop("id", None) + field_id = f.get("field_id") + + # # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce + field = UserFields.query.filter_by(id=field_id).first_or_404() + + # Get the existing field entry if one exists + entry = UserFieldEntries.query.filter_by( + field_id=field.id, user_id=target_user.id + ).first() + if entry: + f["id"] = entry.id + provided_ids.append(entry.id) + + # Extremely dirty hack to prevent deleting previously provided data. + # This needs a better soln. + entries = ( + UserFieldEntries.query.options(load_only("id")) + .filter_by(user_id=target_user.id) + .all() + ) + for entry in entries: + if entry.id not in provided_ids: + fields.append({"id": entry.id}) + else: + provided_ids = [] + for f in fields: + # Remove any existing set + f.pop("id", None) + field_id = f.get("field_id") + value = f.get("value") + + # # Check that we have an existing field for this. May be unnecessary b/c the foriegn key should enforce + field = UserFields.query.filter_by(id=field_id).first_or_404() + + if field.required is True and value.strip() == "": + raise ValidationError( + f"Field '{field.name}' is required", field_names=["fields"] + ) + + if field.editable is False: + raise ValidationError( + f"Field '{field.name}' cannot be editted", + field_names=["fields"], + ) + + # Get the existing field entry if one exists + entry = UserFieldEntries.query.filter_by( + field_id=field.id, user_id=current_user.id + ).first() + + if entry: + f["id"] = entry.id + provided_ids.append(entry.id) + + # Extremely dirty hack to prevent deleting previously provided data. + # This needs a better soln. + entries = ( + UserFieldEntries.query.options(load_only("id")) + .filter_by(user_id=current_user.id) + .all() + ) + for entry in entries: + if entry.id not in provided_ids: + fields.append({"id": entry.id}) + + @post_dump + def process_fields(self, data): + """ + Handle permissions levels for fields. + This is post_dump to manipulate JSON instead of the raw db object + + Admins can see all fields. + Users (self) can see their edittable and public fields + Public (user) can only see public fields + """ + # Gather all possible fields + removed_field_ids = [] + fields = UserFields.query.all() + + # Select fields for removal based on current view and properties of the field + for field in fields: + if self.view == "user": + if field.public is False: + removed_field_ids.append(field.id) + elif self.view == "self": + if field.editable is False and field.public is False: + removed_field_ids.append(field.id) + + # Rebuild fuilds + fields = data.get("fields") + if fields: + data["fields"] = [ + field for field in fields if field["field_id"] not in removed_field_ids + ] + views = { "user": [ "website", @@ -189,6 +315,7 @@ def validate_password_confirmation(self, data): "bracket", "id", "oauth_id", + "fields", ], "self": [ "website", @@ -200,6 +327,7 @@ def validate_password_confirmation(self, data): "id", "oauth_id", "password", + "fields", ], "admin": [ "website", @@ -217,6 +345,7 @@ def validate_password_confirmation(self, data): "password", "type", "verified", + "fields", ], } @@ -226,5 +355,6 @@ def __init__(self, view=None, *args, **kwargs): kwargs["only"] = self.views[view] elif isinstance(view, list): kwargs["only"] = view + self.view = view super(UserSchema, self).__init__(*args, **kwargs) diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index cb1bd88a9..7e9b3409f 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -1,6 +1,5 @@ from flask import Blueprint, render_template -from CTFd.cache import cache, make_cache_key from CTFd.utils import config from CTFd.utils.config.visibility import scores_visible from CTFd.utils.decorators.visibility import check_score_visibility @@ -13,7 +12,6 @@ @scoreboard.route("/scoreboard") @check_score_visibility -@cache.cached(timeout=60, key_prefix=make_cache_key) def listing(): infos = get_infos() diff --git a/CTFd/teams.py b/CTFd/teams.py index 142477c82..fecbd8745 100644 --- a/CTFd/teams.py +++ b/CTFd/teams.py @@ -1,8 +1,8 @@ from flask import Blueprint, redirect, render_template, request, url_for from CTFd.cache import clear_team_session, clear_user_session -from CTFd.models import Teams, db -from CTFd.utils import config, get_config +from CTFd.models import TeamFieldEntries, TeamFields, Teams, db +from CTFd.utils import config, get_config, validators from CTFd.utils.crypto import verify_password from CTFd.utils.decorators import authed_only, ratelimit from CTFd.utils.decorators.modes import require_team_mode @@ -125,6 +125,9 @@ def new(): passphrase = request.form.get("password", "").strip() errors = get_errors() + website = request.form.get("website") + affiliation = request.form.get("affiliation") + user = get_current_user() existing_team = Teams.query.filter_by(name=teamname).first() @@ -133,14 +136,64 @@ def new(): if not teamname: errors.append("That team name is invalid") + # Process additional user fields + fields = {} + for field in TeamFields.query.all(): + fields[field.id] = field + + entries = {} + for field_id, field in fields.items(): + value = request.form.get(f"fields[{field_id}]", "").strip() + if field.required is True and (value is None or value == ""): + errors.append("Please provide all required fields") + break + + # Handle special casing of existing profile fields + if field.name.lower() == "affiliation": + affiliation = value + break + elif field.name.lower() == "website": + website = value + break + + if field.field_type == "boolean": + entries[field_id] = bool(value) + else: + entries[field_id] = value + + if website: + valid_website = validators.validate_url(website) + else: + valid_website = True + + if affiliation: + valid_affiliation = len(affiliation) < 128 + else: + valid_affiliation = True + + if valid_website is False: + errors.append("Websites must be a proper URL starting with http or https") + if valid_affiliation is False: + errors.append("Please provide a shorter affiliation") + if errors: return render_template("teams/new_team.html", errors=errors) team = Teams(name=teamname, password=passphrase, captain_id=user.id) + if website: + team.website = website + if affiliation: + team.affiliation = affiliation + db.session.add(team) db.session.commit() + for field_id, value in entries.items(): + entry = TeamFieldEntries(field_id=field_id, value=value, team_id=team.id) + db.session.add(entry) + db.session.commit() + user.team_id = team.id db.session.commit() diff --git a/CTFd/themes/admin/assets/js/components/comments/CommentBox.vue b/CTFd/themes/admin/assets/js/components/comments/CommentBox.vue new file mode 100644 index 000000000..649202ee3 --- /dev/null +++ b/CTFd/themes/admin/assets/js/components/comments/CommentBox.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/CTFd/themes/admin/assets/js/components/configs/fields/Field.vue b/CTFd/themes/admin/assets/js/components/configs/fields/Field.vue new file mode 100644 index 000000000..484a2c20f --- /dev/null +++ b/CTFd/themes/admin/assets/js/components/configs/fields/Field.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue b/CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue new file mode 100644 index 000000000..c7bddb73c --- /dev/null +++ b/CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue @@ -0,0 +1,82 @@ + + + diff --git a/CTFd/themes/admin/assets/js/pages/challenge.js b/CTFd/themes/admin/assets/js/pages/challenge.js index b9791602d..20fbc596a 100644 --- a/CTFd/themes/admin/assets/js/pages/challenge.js +++ b/CTFd/themes/admin/assets/js/pages/challenge.js @@ -10,6 +10,8 @@ import { addFile, deleteFile } from "../challenges/files"; import { addTag, deleteTag } from "../challenges/tags"; import { addRequirement, deleteRequirement } from "../challenges/requirements"; import { bindMarkdownEditors } from "../styles"; +import Vue from "vue/dist/vue.esm.browser"; +import CommentBox from "../components/comments/CommentBox.vue"; import { showHintModal, editHint, @@ -423,6 +425,14 @@ $(() => { $("#flags-create-select").change(flagTypeSelect); $(".edit-flag").click(editFlagModal); + // Insert CommentBox element + const commentBox = Vue.extend(CommentBox); + let vueContainer = document.createElement("div"); + document.querySelector("#comment-box").appendChild(vueContainer); + new commentBox({ + propsData: { type: "challenge", id: window.CHALLENGE_ID } + }).$mount(vueContainer); + $.get(CTFd.config.urlRoot + "/api/v1/challenges/types", function(response) { const data = response.data; loadChalTemplate(data["standard"]); diff --git a/CTFd/themes/admin/assets/js/pages/configs.js b/CTFd/themes/admin/assets/js/pages/configs.js index c8b631776..13cf7220c 100644 --- a/CTFd/themes/admin/assets/js/pages/configs.js +++ b/CTFd/themes/admin/assets/js/pages/configs.js @@ -9,6 +9,8 @@ import $ from "jquery"; import { ezQuery, ezProgressBar, ezAlert } from "core/ezq"; import CodeMirror from "codemirror"; import "codemirror/mode/htmlmixed/htmlmixed.js"; +import Vue from "vue/dist/vue.esm.browser"; +import FieldList from "../components/configs/fields/FieldList.vue"; function loadTimestamp(place, timestamp) { if (typeof timestamp == "string") { @@ -266,6 +268,17 @@ $(() => { theme_settings_editor.refresh(); }); + $( + "a[href='#legal'], a[href='#tos-config'], a[href='#privacy-policy-config']" + ).on("shown.bs.tab", function(_e) { + $("#tos-config .CodeMirror").each(function(i, el) { + el.CodeMirror.refresh(); + }); + $("#privacy-policy-config .CodeMirror").each(function(i, el) { + el.CodeMirror.refresh(); + }); + }); + $("#theme-settings-modal form").submit(function(e) { e.preventDefault(); theme_settings_editor @@ -360,4 +373,23 @@ $(() => { $("#mail_username_password").toggle(this.checked); }) .change(); + + // Insert FieldList element for users + const fieldList = Vue.extend(FieldList); + let userVueContainer = document.createElement("div"); + document.querySelector("#user-field-list").appendChild(userVueContainer); + new fieldList({ + propsData: { + type: "user" + } + }).$mount(userVueContainer); + + // Insert FieldList element for teams + let teamVueContainer = document.createElement("div"); + document.querySelector("#team-field-list").appendChild(teamVueContainer); + new fieldList({ + propsData: { + type: "team" + } + }).$mount(teamVueContainer); }); diff --git a/CTFd/themes/admin/assets/js/pages/editor.js b/CTFd/themes/admin/assets/js/pages/editor.js index 1f4d39bfb..b51855f5a 100644 --- a/CTFd/themes/admin/assets/js/pages/editor.js +++ b/CTFd/themes/admin/assets/js/pages/editor.js @@ -6,6 +6,8 @@ import CTFd from "core/CTFd"; import CodeMirror from "codemirror"; import "codemirror/mode/htmlmixed/htmlmixed.js"; import { ezToast } from "core/ezq"; +import Vue from "vue/dist/vue.esm.browser"; +import CommentBox from "../components/comments/CommentBox.vue"; function submit_form() { // Save the CodeMirror data to the Textarea @@ -75,4 +77,14 @@ $(() => { $(".preview-page").click(function() { preview_page(); }); + + // Insert CommentBox element + if (window.PAGE_ID) { + const commentBox = Vue.extend(CommentBox); + let vueContainer = document.createElement("div"); + document.querySelector("#comment-box").appendChild(vueContainer); + new commentBox({ + propsData: { type: "page", id: window.PAGE_ID } + }).$mount(vueContainer); + } }); diff --git a/CTFd/themes/admin/assets/js/pages/team.js b/CTFd/themes/admin/assets/js/pages/team.js index 3a43b9c97..bf389d1b1 100644 --- a/CTFd/themes/admin/assets/js/pages/team.js +++ b/CTFd/themes/admin/assets/js/pages/team.js @@ -4,11 +4,26 @@ import CTFd from "core/CTFd"; import { htmlEntities } from "core/utils"; import { ezAlert, ezQuery, ezBadge } from "core/ezq"; import { createGraph, updateGraph } from "core/graphs"; +import Vue from "vue/dist/vue.esm.browser"; +import CommentBox from "../components/comments/CommentBox.vue"; function createTeam(event) { event.preventDefault(); const params = $("#team-info-create-form").serializeJSON(true); + params.fields = []; + + for (const property in params) { + if (property.match(/fields\[\d+\]/)) { + let field = {}; + let id = parseInt(property.slice(7, -1)); + field["field_id"] = id; + field["value"] = params[property]; + params.fields.push(field); + delete params[property]; + } + } + CTFd.fetch("/api/v1/teams", { method: "POST", credentials: "same-origin", @@ -26,15 +41,17 @@ function createTeam(event) { const team_id = response.data.id; window.location = CTFd.config.urlRoot + "/admin/teams/" + team_id; } else { - $("#team-info-form > #results").empty(); + $("#team-info-create-form > #results").empty(); Object.keys(response.errors).forEach(function(key, _index) { - $("#team-info-form > #results").append( + $("#team-info-create-form > #results").append( ezBadge({ type: "error", body: response.errors[key] }) ); - const i = $("#team-info-form").find("input[name={0}]".format(key)); + const i = $("#team-info-create-form").find( + "input[name={0}]".format(key) + ); const input = $(i); input.addClass("input-filled-invalid"); input.removeClass("input-filled-valid"); @@ -45,7 +62,20 @@ function createTeam(event) { function updateTeam(event) { event.preventDefault(); - const params = $("#team-info-edit-form").serializeJSON(true); + let params = $("#team-info-edit-form").serializeJSON(true); + + params.fields = []; + + for (const property in params) { + if (property.match(/fields\[\d+\]/)) { + let field = {}; + let id = parseInt(property.slice(7, -1)); + field["field_id"] = id; + field["value"] = params[property]; + params.fields.push(field); + delete params[property]; + } + } CTFd.fetch("/api/v1/teams/" + window.TEAM_ID, { method: "PATCH", @@ -481,11 +511,30 @@ $(() => { $("#team-info-edit-form").submit(updateTeam); + // Insert CommentBox element + const commentBox = Vue.extend(CommentBox); + let vueContainer = document.createElement("div"); + document.querySelector("#comment-box").appendChild(vueContainer); + new commentBox({ + propsData: { type: "team", id: window.TEAM_ID } + }).$mount(vueContainer); + let type, id, name, account_id; ({ type, id, name, account_id } = window.stats_data); - createGraphs(type, id, name, account_id); - setInterval(() => { - updateGraphs(type, id, name, account_id); - }, 300000); + let intervalId; + $("#team-statistics-modal").on("shown.bs.modal", function(_e) { + createGraphs(type, id, name, account_id); + intervalId = setInterval(() => { + updateGraphs(type, id, name, account_id); + }, 300000); + }); + + $("#team-statistics-modal").on("hidden.bs.modal", function(_e) { + clearInterval(intervalId); + }); + + $(".statistics-team").click(function(_event) { + $("#team-statistics-modal").modal("toggle"); + }); }); diff --git a/CTFd/themes/admin/assets/js/pages/user.js b/CTFd/themes/admin/assets/js/pages/user.js index c146ed194..bfd1b9a62 100644 --- a/CTFd/themes/admin/assets/js/pages/user.js +++ b/CTFd/themes/admin/assets/js/pages/user.js @@ -4,11 +4,26 @@ import CTFd from "core/CTFd"; import { htmlEntities } from "core/utils"; import { ezQuery, ezBadge } from "core/ezq"; import { createGraph, updateGraph } from "core/graphs"; +import Vue from "vue/dist/vue.esm.browser"; +import CommentBox from "../components/comments/CommentBox.vue"; function createUser(event) { event.preventDefault(); const params = $("#user-info-create-form").serializeJSON(true); + params.fields = []; + + for (const property in params) { + if (property.match(/fields\[\d+\]/)) { + let field = {}; + let id = parseInt(property.slice(7, -1)); + field["field_id"] = id; + field["value"] = params[property]; + params.fields.push(field); + delete params[property]; + } + } + // Move the notify value into a GET param let url = "/api/v1/users"; let notify = params.notify; @@ -55,6 +70,19 @@ function updateUser(event) { event.preventDefault(); const params = $("#user-info-edit-form").serializeJSON(true); + params.fields = []; + + for (const property in params) { + if (property.match(/fields\[\d+\]/)) { + let field = {}; + let id = parseInt(property.slice(7, -1)); + field["field_id"] = id; + field["value"] = params[property]; + params.fields.push(field); + delete params[property]; + } + } + CTFd.fetch("/api/v1/users/" + window.USER_ID, { method: "PATCH", credentials: "same-origin", @@ -441,11 +469,30 @@ $(() => { $("#user-info-edit-form").submit(updateUser); $("#user-award-form").submit(awardUser); + // Insert CommentBox element + const commentBox = Vue.extend(CommentBox); + let vueContainer = document.createElement("div"); + document.querySelector("#comment-box").appendChild(vueContainer); + new commentBox({ + propsData: { type: "user", id: window.USER_ID } + }).$mount(vueContainer); + let type, id, name, account_id; ({ type, id, name, account_id } = window.stats_data); - createGraphs(type, id, name, account_id); - setInterval(() => { - updateGraphs(type, id, name, account_id); - }, 300000); + let intervalId; + $("#user-statistics-modal").on("shown.bs.modal", function(_e) { + createGraphs(type, id, name, account_id); + intervalId = setInterval(() => { + updateGraphs(type, id, name, account_id); + }, 300000); + }); + + $("#user-statistics-modal").on("hidden.bs.modal", function(_e) { + clearInterval(intervalId); + }); + + $(".statistics-user").click(function(_event) { + $("#user-statistics-modal").modal("toggle"); + }); }); diff --git a/CTFd/themes/admin/static/js/components.dev.js b/CTFd/themes/admin/static/js/components.dev.js new file mode 100644 index 000000000..f0b0aef9d --- /dev/null +++ b/CTFd/themes/admin/static/js/components.dev.js @@ -0,0 +1,277 @@ +(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["components"],{ + +/***/ "./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue": +/*!************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue ***! + \************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _CommentBox_vue_vue_type_template_id_1fd2c08a_scoped_true___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true& */ \"./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true&\");\n/* harmony import */ var _CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./CommentBox.vue?vue&type=script&lang=js& */ \"./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=script&lang=js&\");\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n/* harmony import */ var _CommentBox_vue_vue_type_style_index_0_id_1fd2c08a_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css& */ \"./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css&\");\n/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(\n _CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _CommentBox_vue_vue_type_template_id_1fd2c08a_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _CommentBox_vue_vue_type_template_id_1fd2c08a_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n \"1fd2c08a\",\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"CTFd/themes/admin/assets/js/components/comments/CommentBox.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=script&lang=js&": +/*!*************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=script&lang=js& ***! + \*************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../node_modules/babel-loader/lib??ref--0!../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./CommentBox.vue?vue&type=script&lang=js& */ \"./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=script&lang=js&\");\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__);\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default.a); \n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css&": +/*!*********************************************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css& ***! + \*********************************************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_style_loader_index_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_style_index_0_id_1fd2c08a_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../node_modules/vue-style-loader!../../../../../../../node_modules/css-loader/dist/cjs.js!../../../../../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css& */ \"./node_modules/vue-style-loader/index.js!./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css&\");\n/* harmony import */ var _node_modules_vue_style_loader_index_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_style_index_0_id_1fd2c08a_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_style_loader_index_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_style_index_0_id_1fd2c08a_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__);\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _node_modules_vue_style_loader_index_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_style_index_0_id_1fd2c08a_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _node_modules_vue_style_loader_index_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_style_index_0_id_1fd2c08a_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_vue_style_loader_index_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_style_index_0_id_1fd2c08a_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0___default.a); \n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true&": +/*!*******************************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true& ***! + \*******************************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_template_id_1fd2c08a_scoped_true___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_template_id_1fd2c08a_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_CommentBox_vue_vue_type_template_id_1fd2c08a_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue": +/*!*************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue ***! + \*************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _Field_vue_vue_type_template_id_30e0f744_scoped_true___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./Field.vue?vue&type=template&id=30e0f744&scoped=true& */ \"./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=template&id=30e0f744&scoped=true&\");\n/* harmony import */ var _Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Field.vue?vue&type=script&lang=js& */ \"./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=script&lang=js&\");\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(\n _Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _Field_vue_vue_type_template_id_30e0f744_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _Field_vue_vue_type_template_id_30e0f744_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n \"30e0f744\",\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"CTFd/themes/admin/assets/js/components/configs/fields/Field.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=script&lang=js&": +/*!**************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=script&lang=js& ***! + \**************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../../node_modules/babel-loader/lib??ref--0!../../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./Field.vue?vue&type=script&lang=js& */ \"./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=script&lang=js&\");\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__);\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default.a); \n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=template&id=30e0f744&scoped=true&": +/*!********************************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=template&id=30e0f744&scoped=true& ***! + \********************************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_template_id_30e0f744_scoped_true___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./Field.vue?vue&type=template&id=30e0f744&scoped=true& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=template&id=30e0f744&scoped=true&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_template_id_30e0f744_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_Field_vue_vue_type_template_id_30e0f744_scoped_true___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue": +/*!*****************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue ***! + \*****************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _FieldList_vue_vue_type_template_id_4b8cc71c___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./FieldList.vue?vue&type=template&id=4b8cc71c& */ \"./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=template&id=4b8cc71c&\");\n/* harmony import */ var _FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./FieldList.vue?vue&type=script&lang=js& */ \"./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=script&lang=js&\");\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(\n _FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _FieldList_vue_vue_type_template_id_4b8cc71c___WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _FieldList_vue_vue_type_template_id_4b8cc71c___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n null,\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=script&lang=js&": +/*!******************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=script&lang=js& ***! + \******************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../../node_modules/babel-loader/lib??ref--0!../../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./FieldList.vue?vue&type=script&lang=js& */ \"./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=script&lang=js&\");\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__);\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default.a); \n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=template&id=4b8cc71c&": +/*!************************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=template&id=4b8cc71c& ***! + \************************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_template_id_4b8cc71c___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./FieldList.vue?vue&type=template&id=4b8cc71c& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=template&id=4b8cc71c&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_template_id_4b8cc71c___WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_FieldList_vue_vue_type_template_id_4b8cc71c___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue": +/*!***********************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue ***! + \***********************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _MediaLibrary_vue_vue_type_template_id_50f8d42a___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./MediaLibrary.vue?vue&type=template&id=50f8d42a& */ \"./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=template&id=50f8d42a&\");\n/* harmony import */ var _MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./MediaLibrary.vue?vue&type=script&lang=js& */ \"./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=script&lang=js&\");\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ \"./node_modules/vue-loader/lib/runtime/componentNormalizer.js\");\n\n\n\n\n\n/* normalize component */\n\nvar component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(\n _MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n _MediaLibrary_vue_vue_type_template_id_50f8d42a___WEBPACK_IMPORTED_MODULE_0__[\"render\"],\n _MediaLibrary_vue_vue_type_template_id_50f8d42a___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"],\n false,\n null,\n null,\n null\n \n)\n\n/* hot reload */\nif (false) { var api; }\ncomponent.options.__file = \"CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue\"\n/* harmony default export */ __webpack_exports__[\"default\"] = (component.exports);\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=script&lang=js&": +/*!************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=script&lang=js& ***! + \************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../node_modules/babel-loader/lib??ref--0!../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./MediaLibrary.vue?vue&type=script&lang=js& */ \"./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=script&lang=js&\");\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__);\n/* harmony reexport (unknown) */ for(var __WEBPACK_IMPORT_KEY__ in _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== 'default') (function(key) { __webpack_require__.d(__webpack_exports__, key, function() { return _node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__[key]; }) }(__WEBPACK_IMPORT_KEY__));\n /* harmony default export */ __webpack_exports__[\"default\"] = (_node_modules_babel_loader_lib_index_js_ref_0_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0___default.a); \n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?"); + +/***/ }), + +/***/ "./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=template&id=50f8d42a&": +/*!******************************************************************************************************!*\ + !*** ./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=template&id=50f8d42a& ***! + \******************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_template_id_50f8d42a___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../../../../../node_modules/vue-loader/lib??vue-loader-options!./MediaLibrary.vue?vue&type=template&id=50f8d42a& */ \"./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=template&id=50f8d42a&\");\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_template_id_50f8d42a___WEBPACK_IMPORTED_MODULE_0__[\"render\"]; });\n\n/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_MediaLibrary_vue_vue_type_template_id_50f8d42a___WEBPACK_IMPORTED_MODULE_0__[\"staticRenderFns\"]; });\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=script&lang=js&": +/*!*******************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=script&lang=js& ***! + \*******************************************************************************************************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +; +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _helpers = _interopRequireDefault(__webpack_require__(/*! core/helpers */ \"./CTFd/themes/core/assets/js/helpers.js\"));\n\nvar _moment = _interopRequireDefault(__webpack_require__(/*! moment */ \"./node_modules/moment/moment.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar _default = {\n props: {\n // These props are passed to the api via query string.\n // See this.getArgs()\n type: String,\n id: Number\n },\n data: function data() {\n return {\n page: 1,\n pages: null,\n next: null,\n prev: null,\n total: null,\n comment: \"\",\n comments: [],\n urlRoot: _CTFd.default.config.urlRoot\n };\n },\n methods: {\n toLocalTime: function toLocalTime(date) {\n return (0, _moment.default)(date).local().format(\"MMMM Do, h:mm:ss A\");\n },\n nextPage: function nextPage() {\n this.page++;\n this.loadComments();\n },\n prevPage: function prevPage() {\n this.page--;\n this.loadComments();\n },\n getArgs: function getArgs() {\n var args = {};\n args[\"\".concat(this.$props.type, \"_id\")] = this.$props.id;\n return args;\n },\n loadComments: function loadComments() {\n var _this = this;\n\n var apiArgs = this.getArgs();\n apiArgs[\"page\"] = this.page;\n apiArgs[\"per_page\"] = 10;\n\n _helpers.default.comments.get_comments(apiArgs).then(function (response) {\n _this.page = response.meta.pagination.page;\n _this.pages = response.meta.pagination.pages;\n _this.next = response.meta.pagination.next;\n _this.prev = response.meta.pagination.prev;\n _this.total = response.meta.pagination.total;\n _this.comments = response.data;\n return _this.comments;\n });\n },\n submitComment: function submitComment() {\n var _this2 = this;\n\n var comment = this.comment.trim();\n\n if (comment.length > 0) {\n _helpers.default.comments.add_comment(comment, this.$props.type, this.getArgs(), function () {\n _this2.loadComments();\n });\n }\n\n this.comment = \"\";\n },\n deleteComment: function deleteComment(commentId) {\n var _this3 = this;\n\n if (confirm(\"Are you sure you'd like to delete this comment?\")) {\n _helpers.default.comments.delete_comment(commentId).then(function (response) {\n if (response.success === true) {\n for (var i = _this3.comments.length - 1; i >= 0; --i) {\n if (_this3.comments[i].id == commentId) {\n _this3.comments.splice(i, 1);\n }\n }\n }\n });\n }\n }\n },\n created: function created() {\n this.loadComments();\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=script&lang=js&": +/*!********************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=script&lang=js& ***! + \********************************************************************************************************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +; +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar _default = {\n props: {\n index: Number,\n initialField: Object\n },\n data: function data() {\n return {\n field: this.initialField\n };\n },\n methods: {\n persistedField: function persistedField() {\n // We're using Math.random() for unique IDs so new items have IDs < 1\n // Real items will have an ID > 1\n return this.field.id >= 1;\n },\n saveField: function saveField() {\n var _this = this;\n\n var body = this.field;\n\n if (this.persistedField()) {\n _CTFd.default.fetch(\"/api/v1/configs/fields/\".concat(this.field.id), {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(body)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success === true) {\n _this.field = response.data;\n (0, _ezq.ezToast)({\n title: \"Success\",\n body: \"Field has been updated!\",\n delay: 1000\n });\n }\n });\n } else {\n _CTFd.default.fetch(\"/api/v1/configs/fields\", {\n method: \"POST\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(body)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success === true) {\n _this.field = response.data;\n (0, _ezq.ezToast)({\n title: \"Success\",\n body: \"Field has been created!\",\n delay: 1000\n });\n }\n });\n }\n },\n deleteField: function deleteField() {\n var _this2 = this;\n\n if (confirm(\"Are you sure you'd like to delete this field?\")) {\n if (this.persistedField()) {\n _CTFd.default.fetch(\"/api/v1/configs/fields/\".concat(this.field.id), {\n method: \"DELETE\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success === true) {\n _this2.$emit(\"remove-field\", _this2.index);\n }\n });\n } else {\n this.$emit(\"remove-field\", this.index);\n }\n }\n }\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=script&lang=js&": +/*!************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=script&lang=js& ***! + \************************************************************************************************************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +; +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _Field = _interopRequireDefault(__webpack_require__(/*! ./Field.vue */ \"./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar _default = {\n name: \"FieldList\",\n components: {\n Field: _Field.default\n },\n props: {\n type: String\n },\n data: function data() {\n return {\n fields: []\n };\n },\n methods: {\n loadFields: function loadFields() {\n var _this = this;\n\n _CTFd.default.fetch(\"/api/v1/configs/fields?type=\".concat(this.type), {\n method: \"GET\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n _this.fields = response.data;\n });\n },\n addField: function addField() {\n this.fields.push({\n id: Math.random(),\n type: this.type,\n field_type: \"text\",\n name: \"\",\n description: \"\",\n editable: false,\n required: false,\n public: false\n });\n },\n removeField: function removeField(index) {\n this.fields.splice(index, 1);\n console.log(this.fields);\n }\n },\n created: function created() {\n this.loadFields();\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=script&lang=js&": +/*!******************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=script&lang=js& ***! + \******************************************************************************************************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +; +eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nvar _helpers = _interopRequireDefault(__webpack_require__(/*! core/helpers */ \"./CTFd/themes/core/assets/js/helpers.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nfunction get_page_files() {\n return _CTFd.default.fetch(\"/api/v1/files?type=page\", {\n credentials: \"same-origin\"\n }).then(function (response) {\n return response.json();\n });\n}\n\nvar _default = {\n props: {\n editor: Object\n },\n data: function data() {\n return {\n files: [],\n selectedFile: null\n };\n },\n methods: {\n getPageFiles: function getPageFiles() {\n var _this = this;\n\n get_page_files().then(function (response) {\n _this.files = response.data;\n return _this.files;\n });\n },\n uploadChosenFiles: function uploadChosenFiles() {\n var _this2 = this;\n\n // TODO: We should reduce the need to interact with the DOM directly.\n // This looks jank and we should be able to remove it.\n var form = document.querySelector(\"#media-library-upload\");\n\n _helpers.default.files.upload(form, {}, function (_data) {\n _this2.getPageFiles();\n });\n },\n selectFile: function selectFile(file) {\n this.selectedFile = file;\n return this.selectedFile;\n },\n buildSelectedFileUrl: function buildSelectedFileUrl() {\n return _CTFd.default.config.urlRoot + \"/files/\" + this.selectedFile.location;\n },\n deleteSelectedFile: function deleteSelectedFile() {\n var _this3 = this;\n\n var file_id = this.selectedFile.id;\n\n if (confirm(\"Are you sure you want to delete this file?\")) {\n _CTFd.default.fetch(\"/api/v1/files/\" + file_id, {\n method: \"DELETE\"\n }).then(function (response) {\n if (response.status === 200) {\n response.json().then(function (object) {\n if (object.success) {\n _this3.getPageFiles();\n\n _this3.selectedFile = null;\n }\n });\n }\n });\n }\n },\n insertSelectedFile: function insertSelectedFile() {\n var editor = this.$props.editor;\n\n if (editor.hasOwnProperty(\"codemirror\")) {\n editor = editor.codemirror;\n }\n\n var doc = editor.getDoc();\n var cursor = doc.getCursor();\n var url = this.buildSelectedFileUrl();\n var img = this.getIconClass(this.selectedFile.location) === \"far fa-file-image\";\n var filename = url.split(\"/\").pop();\n link = \"[{0}]({1})\".format(filename, url);\n\n if (img) {\n link = \"!\" + link;\n }\n\n doc.replaceRange(link, cursor);\n },\n downloadSelectedFile: function downloadSelectedFile() {\n var link = this.buildSelectedFileUrl();\n window.open(link, \"_blank\");\n },\n getIconClass: function getIconClass(filename) {\n var mapping = {\n // Image Files\n png: \"far fa-file-image\",\n jpg: \"far fa-file-image\",\n jpeg: \"far fa-file-image\",\n gif: \"far fa-file-image\",\n bmp: \"far fa-file-image\",\n svg: \"far fa-file-image\",\n // Text Files\n txt: \"far fa-file-alt\",\n // Video Files\n mov: \"far fa-file-video\",\n mp4: \"far fa-file-video\",\n wmv: \"far fa-file-video\",\n flv: \"far fa-file-video\",\n mkv: \"far fa-file-video\",\n avi: \"far fa-file-video\",\n // PDF Files\n pdf: \"far fa-file-pdf\",\n // Audio Files\n mp3: \"far fa-file-sound\",\n wav: \"far fa-file-sound\",\n aac: \"far fa-file-sound\",\n // Archive Files\n zip: \"far fa-file-archive\",\n gz: \"far fa-file-archive\",\n tar: \"far fa-file-archive\",\n \"7z\": \"far fa-file-archive\",\n rar: \"far fa-file-archive\",\n // Code Files\n py: \"far fa-file-code\",\n c: \"far fa-file-code\",\n cpp: \"far fa-file-code\",\n html: \"far fa-file-code\",\n js: \"far fa-file-code\",\n rb: \"far fa-file-code\",\n go: \"far fa-file-code\"\n };\n var ext = filename.split(\".\").pop();\n return mapping[ext] || \"far fa-file\";\n }\n },\n created: function created() {\n return this.getPageFiles();\n }\n};\nexports.default = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css&": +/*!**********************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css& ***! + \**********************************************************************************************************************************************************************************************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +eval("// Imports\nvar ___CSS_LOADER_API_IMPORT___ = __webpack_require__(/*! ../../../../../../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\nexports = ___CSS_LOADER_API_IMPORT___(false);\n// Module\nexports.push([module.i, \"\\n.card .close[data-v-1fd2c08a] {\\n opacity: 0;\\n transition: 0.2s;\\n}\\n.card:hover .close[data-v-1fd2c08a] {\\n opacity: 0.5;\\n}\\n.close[data-v-1fd2c08a]:hover {\\n opacity: 0.75 !important;\\n}\\n.comment-card-leave[data-v-1fd2c08a] {\\n max-height: 200px;\\n}\\n.comment-card-leave-to[data-v-1fd2c08a] {\\n max-height: 0;\\n}\\n.comment-card-active[data-v-1fd2c08a] {\\n position: absolute;\\n}\\n.comment-card-enter-active[data-v-1fd2c08a],\\n.comment-card-move[data-v-1fd2c08a],\\n.comment-card-leave-active[data-v-1fd2c08a] {\\n transition: all 0.3s;\\n}\\n\", \"\"]);\n// Exports\nmodule.exports = exports;\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true&": +/*!*************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=template&id=1fd2c08a&scoped=true& ***! + \*************************************************************************************************************************************************************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"div\", [\n _c(\"div\", { staticClass: \"row mb-3\" }, [\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"comment\" }, [\n _c(\"textarea\", {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model.lazy\",\n value: _vm.comment,\n expression: \"comment\",\n modifiers: { lazy: true }\n }\n ],\n staticClass: \"form-control mb-2\",\n attrs: {\n rows: \"2\",\n id: \"comment-input\",\n placeholder: \"Add comment\"\n },\n domProps: { value: _vm.comment },\n on: {\n change: function($event) {\n _vm.comment = $event.target.value\n }\n }\n }),\n _vm._v(\" \"),\n _c(\n \"button\",\n {\n staticClass: \"btn btn-sm btn-success btn-outlined float-right\",\n attrs: { type: \"submit\" },\n on: {\n click: function($event) {\n return _vm.submitComment()\n }\n }\n },\n [_vm._v(\"\\n Comment\\n \")]\n )\n ])\n ])\n ]),\n _vm._v(\" \"),\n _vm.pages > 1\n ? _c(\"div\", { staticClass: \"row\" }, [\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"text-center\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-link p-0\",\n attrs: { type: \"button\", disabled: _vm.prev ? false : true },\n on: {\n click: function($event) {\n return _vm.prevPage()\n }\n }\n },\n [_vm._v(\"\\n <<<\\n \")]\n ),\n _vm._v(\" \"),\n _c(\n \"button\",\n {\n staticClass: \"btn btn-link p-0\",\n attrs: { type: \"button\", disabled: _vm.next ? false : true },\n on: {\n click: function($event) {\n return _vm.nextPage()\n }\n }\n },\n [_vm._v(\"\\n >>>\\n \")]\n )\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"text-center\" }, [\n _c(\"small\", { staticClass: \"text-muted\" }, [\n _vm._v(\n \"Page \" +\n _vm._s(_vm.page) +\n \" of \" +\n _vm._s(_vm.total) +\n \" comments\"\n )\n ])\n ])\n ])\n ])\n : _vm._e(),\n _vm._v(\" \"),\n _c(\n \"div\",\n { staticClass: \"comments\" },\n [\n _c(\n \"transition-group\",\n { attrs: { name: \"comment-card\" } },\n _vm._l(_vm.comments, function(comment) {\n return _c(\n \"div\",\n { key: comment.id, staticClass: \"comment-card card mb-2\" },\n [\n _c(\"div\", { staticClass: \"card-body pl-0 pb-0 pt-2 pr-2\" }, [\n _c(\n \"button\",\n {\n staticClass: \"close float-right\",\n attrs: { type: \"button\", \"aria-label\": \"Close\" },\n on: {\n click: function($event) {\n return _vm.deleteComment(comment.id)\n }\n }\n },\n [\n _c(\"span\", { attrs: { \"aria-hidden\": \"true\" } }, [\n _vm._v(\"×\")\n ])\n ]\n )\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"card-body\" }, [\n _c(\"div\", {\n staticClass: \"card-text\",\n domProps: { innerHTML: _vm._s(comment.html) }\n }),\n _vm._v(\" \"),\n _c(\"small\", { staticClass: \"text-muted float-left\" }, [\n _c(\"span\", [\n _c(\n \"a\",\n {\n attrs: {\n href:\n _vm.urlRoot + \"/admin/users/\" + comment.author_id\n }\n },\n [_vm._v(_vm._s(comment.author.name))]\n )\n ])\n ]),\n _vm._v(\" \"),\n _c(\"small\", { staticClass: \"text-muted float-right\" }, [\n _c(\"span\", { staticClass: \"float-right\" }, [\n _vm._v(_vm._s(_vm.toLocalTime(comment.date)))\n ])\n ])\n ])\n ]\n )\n }),\n 0\n )\n ],\n 1\n ),\n _vm._v(\" \"),\n _vm.pages > 1\n ? _c(\"div\", { staticClass: \"row\" }, [\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"text-center\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-link p-0\",\n attrs: { type: \"button\", disabled: _vm.prev ? false : true },\n on: {\n click: function($event) {\n return _vm.prevPage()\n }\n }\n },\n [_vm._v(\"\\n <<<\\n \")]\n ),\n _vm._v(\" \"),\n _c(\n \"button\",\n {\n staticClass: \"btn btn-link p-0\",\n attrs: { type: \"button\", disabled: _vm.next ? false : true },\n on: {\n click: function($event) {\n return _vm.nextPage()\n }\n }\n },\n [_vm._v(\"\\n >>>\\n \")]\n )\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"text-center\" }, [\n _c(\"small\", { staticClass: \"text-muted\" }, [\n _vm._v(\n \"Page \" +\n _vm._s(_vm.page) +\n \" of \" +\n _vm._s(_vm.total) +\n \" comments\"\n )\n ])\n ])\n ])\n ])\n : _vm._e()\n ])\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=template&id=30e0f744&scoped=true&": +/*!**************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?vue&type=template&id=30e0f744&scoped=true& ***! + \**************************************************************************************************************************************************************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"div\", { staticClass: \"border-bottom\" }, [\n _c(\"div\", [\n _c(\n \"button\",\n {\n staticClass: \"close float-right\",\n attrs: { type: \"button\", \"aria-label\": \"Close\" },\n on: {\n click: function($event) {\n return _vm.deleteField()\n }\n }\n },\n [_c(\"span\", { attrs: { \"aria-hidden\": \"true\" } }, [_vm._v(\"×\")])]\n )\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"row\" }, [\n _c(\"div\", { staticClass: \"col-md-3\" }, [\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\"label\", [_vm._v(\"Field Type\")]),\n _vm._v(\" \"),\n _c(\n \"select\",\n {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model.lazy\",\n value: _vm.field.field_type,\n expression: \"field.field_type\",\n modifiers: { lazy: true }\n }\n ],\n staticClass: \"form-control custom-select\",\n on: {\n change: function($event) {\n var $$selectedVal = Array.prototype.filter\n .call($event.target.options, function(o) {\n return o.selected\n })\n .map(function(o) {\n var val = \"_value\" in o ? o._value : o.value\n return val\n })\n _vm.$set(\n _vm.field,\n \"field_type\",\n $event.target.multiple ? $$selectedVal : $$selectedVal[0]\n )\n }\n }\n },\n [\n _c(\"option\", { attrs: { value: \"text\" } }, [\n _vm._v(\"Text Field\")\n ]),\n _vm._v(\" \"),\n _c(\"option\", { attrs: { value: \"boolean\" } }, [\n _vm._v(\"Checkbox\")\n ])\n ]\n ),\n _vm._v(\" \"),\n _c(\"small\", { staticClass: \"form-text text-muted\" }, [\n _vm._v(\"Type of field shown to the user\")\n ])\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"col-md-9\" }, [\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\"label\", [_vm._v(\"Field Name\")]),\n _vm._v(\" \"),\n _c(\"input\", {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model.lazy\",\n value: _vm.field.name,\n expression: \"field.name\",\n modifiers: { lazy: true }\n }\n ],\n staticClass: \"form-control\",\n attrs: { type: \"text\" },\n domProps: { value: _vm.field.name },\n on: {\n change: function($event) {\n return _vm.$set(_vm.field, \"name\", $event.target.value)\n }\n }\n }),\n _vm._v(\" \"),\n _c(\"small\", { staticClass: \"form-text text-muted\" }, [\n _vm._v(\"Field name\")\n ])\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\"label\", [_vm._v(\"Field Description\")]),\n _vm._v(\" \"),\n _c(\"input\", {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model.lazy\",\n value: _vm.field.description,\n expression: \"field.description\",\n modifiers: { lazy: true }\n }\n ],\n staticClass: \"form-control\",\n attrs: { type: \"text\" },\n domProps: { value: _vm.field.description },\n on: {\n change: function($event) {\n return _vm.$set(_vm.field, \"description\", $event.target.value)\n }\n }\n }),\n _vm._v(\" \"),\n _c(\n \"small\",\n { staticClass: \"form-text text-muted\", attrs: { id: \"emailHelp\" } },\n [_vm._v(\"Field Description\")]\n )\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"form-check\" }, [\n _c(\"label\", { staticClass: \"form-check-label\" }, [\n _c(\"input\", {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model.lazy\",\n value: _vm.field.editable,\n expression: \"field.editable\",\n modifiers: { lazy: true }\n }\n ],\n staticClass: \"form-check-input\",\n attrs: { type: \"checkbox\" },\n domProps: {\n checked: Array.isArray(_vm.field.editable)\n ? _vm._i(_vm.field.editable, null) > -1\n : _vm.field.editable\n },\n on: {\n change: function($event) {\n var $$a = _vm.field.editable,\n $$el = $event.target,\n $$c = $$el.checked ? true : false\n if (Array.isArray($$a)) {\n var $$v = null,\n $$i = _vm._i($$a, $$v)\n if ($$el.checked) {\n $$i < 0 &&\n _vm.$set(_vm.field, \"editable\", $$a.concat([$$v]))\n } else {\n $$i > -1 &&\n _vm.$set(\n _vm.field,\n \"editable\",\n $$a.slice(0, $$i).concat($$a.slice($$i + 1))\n )\n }\n } else {\n _vm.$set(_vm.field, \"editable\", $$c)\n }\n }\n }\n }),\n _vm._v(\"\\n Editable by user in profile\\n \")\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"form-check\" }, [\n _c(\"label\", { staticClass: \"form-check-label\" }, [\n _c(\"input\", {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model.lazy\",\n value: _vm.field.required,\n expression: \"field.required\",\n modifiers: { lazy: true }\n }\n ],\n staticClass: \"form-check-input\",\n attrs: { type: \"checkbox\" },\n domProps: {\n checked: Array.isArray(_vm.field.required)\n ? _vm._i(_vm.field.required, null) > -1\n : _vm.field.required\n },\n on: {\n change: function($event) {\n var $$a = _vm.field.required,\n $$el = $event.target,\n $$c = $$el.checked ? true : false\n if (Array.isArray($$a)) {\n var $$v = null,\n $$i = _vm._i($$a, $$v)\n if ($$el.checked) {\n $$i < 0 &&\n _vm.$set(_vm.field, \"required\", $$a.concat([$$v]))\n } else {\n $$i > -1 &&\n _vm.$set(\n _vm.field,\n \"required\",\n $$a.slice(0, $$i).concat($$a.slice($$i + 1))\n )\n }\n } else {\n _vm.$set(_vm.field, \"required\", $$c)\n }\n }\n }\n }),\n _vm._v(\"\\n Required on registration\\n \")\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"form-check\" }, [\n _c(\"label\", { staticClass: \"form-check-label\" }, [\n _c(\"input\", {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model.lazy\",\n value: _vm.field.public,\n expression: \"field.public\",\n modifiers: { lazy: true }\n }\n ],\n staticClass: \"form-check-input\",\n attrs: { type: \"checkbox\" },\n domProps: {\n checked: Array.isArray(_vm.field.public)\n ? _vm._i(_vm.field.public, null) > -1\n : _vm.field.public\n },\n on: {\n change: function($event) {\n var $$a = _vm.field.public,\n $$el = $event.target,\n $$c = $$el.checked ? true : false\n if (Array.isArray($$a)) {\n var $$v = null,\n $$i = _vm._i($$a, $$v)\n if ($$el.checked) {\n $$i < 0 &&\n _vm.$set(_vm.field, \"public\", $$a.concat([$$v]))\n } else {\n $$i > -1 &&\n _vm.$set(\n _vm.field,\n \"public\",\n $$a.slice(0, $$i).concat($$a.slice($$i + 1))\n )\n }\n } else {\n _vm.$set(_vm.field, \"public\", $$c)\n }\n }\n }\n }),\n _vm._v(\"\\n Shown on public profile\\n \")\n ])\n ])\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"row pb-3\" }, [\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"div\", { staticClass: \"d-block\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-sm btn-success btn-outlined float-right\",\n attrs: { type: \"button\" },\n on: {\n click: function($event) {\n return _vm.saveField()\n }\n }\n },\n [_vm._v(\"\\n Save\\n \")]\n )\n ])\n ])\n ])\n ])\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/Field.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=template&id=4b8cc71c&": +/*!******************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?vue&type=template&id=4b8cc71c& ***! + \******************************************************************************************************************************************************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _vm._l(_vm.fields, function(field, index) {\n return _c(\n \"div\",\n { key: field.id, staticClass: \"mb-5\" },\n [\n _c(\"Field\", {\n attrs: { index: index, initialField: _vm.fields[index] },\n on: {\n \"update:initialField\": function($event) {\n return _vm.$set(_vm.fields, index, $event)\n },\n \"update:initial-field\": function($event) {\n return _vm.$set(_vm.fields, index, $event)\n },\n \"remove-field\": _vm.removeField\n }\n })\n ],\n 1\n )\n }),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"row\" }, [\n _c(\"div\", { staticClass: \"col text-center\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-sm btn-success btn-outlined m-auto\",\n attrs: { type: \"button\" },\n on: {\n click: function($event) {\n return _vm.addField()\n }\n }\n },\n [_vm._v(\"\\n Add New Field\\n \")]\n )\n ])\n ])\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=template&id=50f8d42a&": +/*!************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?vue&type=template&id=50f8d42a& ***! + \************************************************************************************************************************************************************************************************************************************/ +/*! exports provided: render, staticRenderFns */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n { staticClass: \"modal fade\", attrs: { id: \"media-modal\", tabindex: \"-1\" } },\n [\n _c(\"div\", { staticClass: \"modal-dialog modal-lg\" }, [\n _c(\"div\", { staticClass: \"modal-content\" }, [\n _vm._m(0),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"modal-body\" }, [\n _c(\"div\", { staticClass: \"modal-header\" }, [\n _c(\"div\", { staticClass: \"container\" }, [\n _c(\"div\", { staticClass: \"row mh-100\" }, [\n _c(\n \"div\",\n {\n staticClass: \"col-md-6\",\n attrs: { id: \"media-library-list\" }\n },\n _vm._l(_vm.files, function(file) {\n return _c(\n \"div\",\n { key: file.id, staticClass: \"media-item-wrapper\" },\n [\n _c(\n \"a\",\n {\n attrs: { href: \"javascript:void(0)\" },\n on: {\n click: function($event) {\n _vm.selectFile(file)\n return false\n }\n }\n },\n [\n _c(\"i\", {\n class: _vm.getIconClass(file.location),\n attrs: { \"aria-hidden\": \"true\" }\n }),\n _vm._v(\" \"),\n _c(\"small\", { staticClass: \"media-item-title\" }, [\n _vm._v(_vm._s(file.location.split(\"/\").pop()))\n ])\n ]\n )\n ]\n )\n }),\n 0\n ),\n _vm._v(\" \"),\n _c(\n \"div\",\n {\n staticClass: \"col-md-6\",\n attrs: { id: \"media-library-details\" }\n },\n [\n _c(\"h4\", { staticClass: \"text-center\" }, [\n _vm._v(\"Media Details\")\n ]),\n _vm._v(\" \"),\n _c(\"div\", { attrs: { id: \"media-item\" } }, [\n _c(\n \"div\",\n {\n staticClass: \"text-center\",\n attrs: { id: \"media-icon\" }\n },\n [\n this.selectedFile\n ? _c(\"div\", [\n _vm.getIconClass(\n this.selectedFile.location\n ) === \"far fa-file-image\"\n ? _c(\"div\", [\n _c(\"img\", {\n staticStyle: {\n \"max-width\": \"100%\",\n \"max-height\": \"100%\",\n \"object-fit\": \"contain\"\n },\n attrs: {\n src: _vm.buildSelectedFileUrl()\n }\n })\n ])\n : _c(\"div\", [\n _c(\"i\", {\n class:\n _vm.getIconClass(\n this.selectedFile.location\n ) + \" fa-4x\",\n attrs: { \"aria-hidden\": \"true\" }\n })\n ])\n ])\n : _vm._e()\n ]\n ),\n _vm._v(\" \"),\n _c(\"br\"),\n _vm._v(\" \"),\n this.selectedFile\n ? _c(\n \"div\",\n {\n staticClass: \"text-center\",\n attrs: { id: \"media-filename\" }\n },\n [\n _c(\n \"a\",\n {\n attrs: {\n href: _vm.buildSelectedFileUrl(),\n target: \"_blank\"\n }\n },\n [\n _vm._v(\n \"\\n \" +\n _vm._s(\n this.selectedFile.location\n .split(\"/\")\n .pop()\n ) +\n \"\\n \"\n )\n ]\n )\n ]\n )\n : _vm._e(),\n _vm._v(\" \"),\n _c(\"br\"),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"form-group\" }, [\n this.selectedFile\n ? _c(\"div\", [\n _vm._v(\n \"\\n Link:\\n \"\n ),\n _c(\"input\", {\n staticClass: \"form-control\",\n attrs: {\n type: \"text\",\n id: \"media-link\",\n readonly: \"\"\n },\n domProps: {\n value: _vm.buildSelectedFileUrl()\n }\n })\n ])\n : _c(\"div\", [\n _vm._v(\n \"\\n Link:\\n \"\n ),\n _c(\"input\", {\n staticClass: \"form-control\",\n attrs: {\n type: \"text\",\n id: \"media-link\",\n readonly: \"\"\n }\n })\n ])\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"form-group text-center\" }, [\n _c(\"div\", { staticClass: \"row\" }, [\n _c(\"div\", { staticClass: \"col-md-6\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-success w-100\",\n attrs: {\n id: \"media-insert\",\n \"data-toggle\": \"tooltip\",\n \"data-placement\": \"top\",\n title: \"Insert link into editor\"\n },\n on: { click: _vm.insertSelectedFile }\n },\n [\n _vm._v(\n \"\\n Insert\\n \"\n )\n ]\n )\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"col-md-3\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-primary w-100\",\n attrs: {\n id: \"media-download\",\n \"data-toggle\": \"tooltip\",\n \"data-placement\": \"top\",\n title: \"Download file\"\n },\n on: { click: _vm.downloadSelectedFile }\n },\n [_c(\"i\", { staticClass: \"fas fa-download\" })]\n )\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"col-md-3\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-danger w-100\",\n attrs: {\n id: \"media-delete\",\n \"data-toggle\": \"tooltip\",\n \"data-placement\": \"top\",\n title: \"Delete file\"\n },\n on: { click: _vm.deleteSelectedFile }\n },\n [_c(\"i\", { staticClass: \"far fa-trash-alt\" })]\n )\n ])\n ])\n ])\n ])\n ]\n )\n ])\n ])\n ]),\n _vm._v(\" \"),\n _vm._m(1)\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"modal-footer\" }, [\n _c(\"div\", { staticClass: \"float-right\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-primary media-upload-button\",\n attrs: { type: \"submit\" },\n on: { click: _vm.uploadChosenFiles }\n },\n [_vm._v(\"\\n Upload\\n \")]\n )\n ])\n ])\n ])\n ])\n ]\n )\n}\nvar staticRenderFns = [\n function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"div\", { staticClass: \"modal-header\" }, [\n _c(\"div\", { staticClass: \"container\" }, [\n _c(\"div\", { staticClass: \"row\" }, [\n _c(\"div\", { staticClass: \"col-md-12\" }, [\n _c(\"h3\", { staticClass: \"text-center\" }, [_vm._v(\"Media Library\")])\n ])\n ])\n ]),\n _vm._v(\" \"),\n _c(\n \"button\",\n {\n staticClass: \"close\",\n attrs: {\n type: \"button\",\n \"data-dismiss\": \"modal\",\n \"aria-label\": \"Close\"\n }\n },\n [_c(\"span\", { attrs: { \"aria-hidden\": \"true\" } }, [_vm._v(\"×\")])]\n )\n ])\n },\n function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"form\",\n { attrs: { id: \"media-library-upload\", enctype: \"multipart/form-data\" } },\n [\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\"label\", { attrs: { for: \"media-files\" } }, [\n _vm._v(\"\\n Upload Files\\n \")\n ]),\n _vm._v(\" \"),\n _c(\"input\", {\n staticClass: \"form-control-file\",\n attrs: {\n type: \"file\",\n name: \"file\",\n id: \"media-files\",\n multiple: \"\"\n }\n }),\n _vm._v(\" \"),\n _c(\"sub\", { staticClass: \"help-block\" }, [\n _vm._v(\n \"\\n Attach multiple files using Control+Click or Cmd+Click.\\n \"\n )\n ])\n ]),\n _vm._v(\" \"),\n _c(\"input\", { attrs: { type: \"hidden\", value: \"page\", name: \"type\" } })\n ]\n )\n }\n]\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/files/MediaLibrary.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); + +/***/ }), + +/***/ "./node_modules/vue-style-loader/index.js!./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/vue-loader/lib/index.js?!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css&": +/*!******************************************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/vue-style-loader!./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/vue-loader/lib??vue-loader-options!./CTFd/themes/admin/assets/js/components/comments/CommentBox.vue?vue&type=style&index=0&id=1fd2c08a&scoped=true&lang=css& ***! + \******************************************************************************************************************************************************************************************************************************************************************************************************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +eval("// style-loader: Adds some css to the DOM by adding a \\n\";\n }\n\n theme_header_editor.getDoc().setValue(new_css);\n });\n (0, _jquery.default)(\".start-date\").change(function () {\n loadDateValues(\"start\");\n });\n (0, _jquery.default)(\".end-date\").change(function () {\n loadDateValues(\"end\");\n });\n (0, _jquery.default)(\".freeze-date\").change(function () {\n loadDateValues(\"freeze\");\n });\n var start = (0, _jquery.default)(\"#start\").val();\n var end = (0, _jquery.default)(\"#end\").val();\n var freeze = (0, _jquery.default)(\"#freeze\").val();\n\n if (start) {\n loadTimestamp(\"start\", start);\n }\n\n if (end) {\n loadTimestamp(\"end\", end);\n }\n\n if (freeze) {\n loadTimestamp(\"freeze\", freeze);\n } // Toggle username and password based on stored value\n\n\n (0, _jquery.default)(\"#mail_useauth\").change(function () {\n (0, _jquery.default)(\"#mail_username_password\").toggle(this.checked);\n }).change();\n});\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/pages/configs.js?"); +eval("\n\n__webpack_require__(/*! ./main */ \"./CTFd/themes/admin/assets/js/pages/main.js\");\n\n__webpack_require__(/*! core/utils */ \"./CTFd/themes/core/assets/js/utils.js\");\n\n__webpack_require__(/*! bootstrap/js/dist/tab */ \"./node_modules/bootstrap/js/dist/tab.js\");\n\nvar _momentTimezone = _interopRequireDefault(__webpack_require__(/*! moment-timezone */ \"./node_modules/moment-timezone/index.js\"));\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nvar _helpers = _interopRequireDefault(__webpack_require__(/*! core/helpers */ \"./CTFd/themes/core/assets/js/helpers.js\"));\n\nvar _jquery = _interopRequireDefault(__webpack_require__(/*! jquery */ \"./node_modules/jquery/dist/jquery.js\"));\n\nvar _ezq = __webpack_require__(/*! core/ezq */ \"./CTFd/themes/core/assets/js/ezq.js\");\n\nvar _codemirror = _interopRequireDefault(__webpack_require__(/*! codemirror */ \"./node_modules/codemirror/lib/codemirror.js\"));\n\n__webpack_require__(/*! codemirror/mode/htmlmixed/htmlmixed.js */ \"./node_modules/codemirror/mode/htmlmixed/htmlmixed.js\");\n\nvar _vueEsm = _interopRequireDefault(__webpack_require__(/*! vue/dist/vue.esm.browser */ \"./node_modules/vue/dist/vue.esm.browser.js\"));\n\nvar _FieldList = _interopRequireDefault(__webpack_require__(/*! ../components/configs/fields/FieldList.vue */ \"./CTFd/themes/admin/assets/js/components/configs/fields/FieldList.vue\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction loadTimestamp(place, timestamp) {\n if (typeof timestamp == \"string\") {\n timestamp = parseInt(timestamp, 10);\n }\n\n var m = (0, _momentTimezone.default)(timestamp * 1000);\n (0, _jquery.default)(\"#\" + place + \"-month\").val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/)\n\n (0, _jquery.default)(\"#\" + place + \"-day\").val(m.date());\n (0, _jquery.default)(\"#\" + place + \"-year\").val(m.year());\n (0, _jquery.default)(\"#\" + place + \"-hour\").val(m.hour());\n (0, _jquery.default)(\"#\" + place + \"-minute\").val(m.minute());\n loadDateValues(place);\n}\n\nfunction loadDateValues(place) {\n var month = (0, _jquery.default)(\"#\" + place + \"-month\").val();\n var day = (0, _jquery.default)(\"#\" + place + \"-day\").val();\n var year = (0, _jquery.default)(\"#\" + place + \"-year\").val();\n var hour = (0, _jquery.default)(\"#\" + place + \"-hour\").val();\n var minute = (0, _jquery.default)(\"#\" + place + \"-minute\").val();\n var timezone = (0, _jquery.default)(\"#\" + place + \"-timezone\").val();\n var utc = convertDateToMoment(month, day, year, hour, minute);\n\n if (isNaN(utc.unix())) {\n (0, _jquery.default)(\"#\" + place).val(\"\");\n (0, _jquery.default)(\"#\" + place + \"-local\").val(\"\");\n (0, _jquery.default)(\"#\" + place + \"-zonetime\").val(\"\");\n } else {\n (0, _jquery.default)(\"#\" + place).val(utc.unix());\n (0, _jquery.default)(\"#\" + place + \"-local\").val(utc.local().format(\"dddd, MMMM Do YYYY, h:mm:ss a zz\"));\n (0, _jquery.default)(\"#\" + place + \"-zonetime\").val(utc.tz(timezone).format(\"dddd, MMMM Do YYYY, h:mm:ss a zz\"));\n }\n}\n\nfunction convertDateToMoment(month, day, year, hour, minute) {\n var month_num = month.toString();\n\n if (month_num.length == 1) {\n month_num = \"0\" + month_num;\n }\n\n var day_str = day.toString();\n\n if (day_str.length == 1) {\n day_str = \"0\" + day_str;\n }\n\n var hour_str = hour.toString();\n\n if (hour_str.length == 1) {\n hour_str = \"0\" + hour_str;\n }\n\n var min_str = minute.toString();\n\n if (min_str.length == 1) {\n min_str = \"0\" + min_str;\n } // 2013-02-08 24:00\n\n\n var date_string = year.toString() + \"-\" + month_num + \"-\" + day_str + \" \" + hour_str + \":\" + min_str + \":00\";\n return (0, _momentTimezone.default)(date_string, _momentTimezone.default.ISO_8601);\n}\n\nfunction updateConfigs(event) {\n event.preventDefault();\n var obj = (0, _jquery.default)(this).serializeJSON();\n var params = {};\n\n if (obj.mail_useauth === false) {\n obj.mail_username = null;\n obj.mail_password = null;\n } else {\n if (obj.mail_username === \"\") {\n delete obj.mail_username;\n }\n\n if (obj.mail_password === \"\") {\n delete obj.mail_password;\n }\n }\n\n Object.keys(obj).forEach(function (x) {\n if (obj[x] === \"true\") {\n params[x] = true;\n } else if (obj[x] === \"false\") {\n params[x] = false;\n } else {\n params[x] = obj[x];\n }\n });\n\n _CTFd.default.api.patch_config_list({}, params).then(function (_response) {\n window.location.reload();\n });\n}\n\nfunction uploadLogo(event) {\n event.preventDefault();\n var form = event.target;\n\n _helpers.default.files.upload(form, {}, function (response) {\n var f = response.data[0];\n var params = {\n value: f.location\n };\n\n _CTFd.default.fetch(\"/api/v1/configs/ctf_logo\", {\n method: \"PATCH\",\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n window.location.reload();\n } else {\n (0, _ezq.ezAlert)({\n title: \"Error!\",\n body: \"Logo uploading failed!\",\n button: \"Okay\"\n });\n }\n });\n });\n}\n\nfunction removeLogo() {\n (0, _ezq.ezQuery)({\n title: \"Remove logo\",\n body: \"Are you sure you'd like to remove the CTF logo?\",\n success: function success() {\n var params = {\n value: null\n };\n\n _CTFd.default.api.patch_config({\n configKey: \"ctf_logo\"\n }, params).then(function (_response) {\n window.location.reload();\n });\n }\n });\n}\n\nfunction importConfig(event) {\n event.preventDefault();\n var import_file = document.getElementById(\"import-file\").files[0];\n var form_data = new FormData();\n form_data.append(\"backup\", import_file);\n form_data.append(\"nonce\", _CTFd.default.config.csrfNonce);\n var pg = (0, _ezq.ezProgressBar)({\n width: 0,\n title: \"Upload Progress\"\n });\n\n _jquery.default.ajax({\n url: _CTFd.default.config.urlRoot + \"/admin/import\",\n type: \"POST\",\n data: form_data,\n processData: false,\n contentType: false,\n statusCode: {\n 500: function _(resp) {\n alert(resp.responseText);\n }\n },\n xhr: function xhr() {\n var xhr = _jquery.default.ajaxSettings.xhr();\n\n xhr.upload.onprogress = function (e) {\n if (e.lengthComputable) {\n var width = e.loaded / e.total * 100;\n pg = (0, _ezq.ezProgressBar)({\n target: pg,\n width: width\n });\n }\n };\n\n return xhr;\n },\n success: function success(_data) {\n pg = (0, _ezq.ezProgressBar)({\n target: pg,\n width: 100\n });\n setTimeout(function () {\n pg.modal(\"hide\");\n }, 500);\n setTimeout(function () {\n window.location.reload();\n }, 700);\n }\n });\n}\n\nfunction exportConfig(event) {\n event.preventDefault();\n window.location.href = (0, _jquery.default)(this).attr(\"href\");\n}\n\nfunction insertTimezones(target) {\n var current = (0, _jquery.default)(\"