Repository: CTFd/CTFd
Pull request #786 (2018-12-07 02:15:01)

Fix hint loading for admins with /api/v1/hints/<id>?preview=true
* Fix hint loading for admins by adding /api/v1/hints/<id>?preview=true for use by admins
* Add tests for admin Hint preview
* This also starts integrating the core and admin themes meaning that the core theme cannot be removed from CTFd and is available for usage by other themes

codecov[bot]: # [Codecov](https://codecov.io/gh/CTFd/CTFd/pull/786?src=pr&el=h1) Report
> Merging [#786](https://codecov.io/gh/CTFd/CTFd/pull/786?src=pr&el=desc) into [master](https://codecov.io/gh/CTFd/CTFd/commit/afdfaa15da3c40d44f18563a8beeb8c499823524?src=pr&el=desc) will **decrease** coverage by `0.03%`.
> The diff coverage is `50%`.

[![Impacted file tree graph](https://codecov.io/gh/CTFd/CTFd/pull/786/graphs/tree.svg?width=650&token=IfPqkWkt9A&height=150&src=pr)](https://codecov.io/gh/CTFd/CTFd/pull/786?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #786      +/-   ##
==========================================
- Coverage   84.21%   84.18%   -0.04%     
==========================================
  Files          91       91              
  Lines        4504     4507       +3     
==========================================
+ Hits         3793     3794       +1     
- Misses        711      713       +2
```


| [Impacted Files](https://codecov.io/gh/CTFd/CTFd/pull/786?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [CTFd/api/v1/hints.py](https://codecov.io/gh/CTFd/CTFd/pull/786/diff?src=pr&el=tree#diff-Q1RGZC9hcGkvdjEvaGludHMucHk=) | `90.62% <50%> (-2.82%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/CTFd/CTFd/pull/786?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/CTFd/CTFd/pull/786?src=pr&el=footer). Last update [afdfaa1...e502f1e](https://codecov.io/gh/CTFd/CTFd/pull/786?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CTFd/api/v1/hints.py b/CTFd/api/v1/hints.py
index eacfb968..236295cd 100644
--- a/CTFd/api/v1/hints.py
+++ b/CTFd/api/v1/hints.py
@@ -3,7 +3,7 @@ from flask_restplus import Namespace, Resource
 from CTFd.models import db, Hints, HintUnlocks
 from CTFd.plugins.challenges import get_chal_class
 from CTFd.utils.dates import ctf_ended
-from CTFd.utils.user import get_current_user
+from CTFd.utils.user import get_current_user, is_admin
 from CTFd.schemas.hints import HintSchema
 from CTFd.utils.decorators import (
     during_ctf_time_only,
@@ -75,6 +75,10 @@ class Hint(Resource):
             if unlocked:
                 view = 'unlocked'
 
+        if is_admin():
+            if request.args.get('preview', False):
+                view = 'admin'
+
         response = HintSchema(view=view).dump(hint)
 
         if response.errors:
diff --git a/CTFd/themes/admin/static/js/challenges/hints.js b/CTFd/themes/admin/static/js/challenges/hints.js
index 361be5d6..e006ce2c 100644
--- a/CTFd/themes/admin/static/js/challenges/hints.js
+++ b/CTFd/themes/admin/static/js/challenges/hints.js
@@ -1,6 +1,55 @@
+function hint(id) {
+    return fetch(script_root + '/api/v1/hints/' + id + '?preview=true', {
+        method: 'GET',
+        credentials: 'same-origin',
+        headers: {
+            'Accept': 'application/json',
+            'Content-Type': 'application/json'
+        }
+    }).then(function (response) {
+        return response.json();
+    });
+}
+
+function loadhint(hintid) {
+    var md = window.markdownit({
+        html: true,
+    });
+
+    hint(hintid).then(function (response) {
+        if (response.data.content) {
+            ezal({
+                title: "Hint",
+                body: md.render(response.data.content),
+                button: "Got it!"
+            });
+        } else {
+            ezal({
+                title: "Error",
+                body: "Error loading hint!",
+                button: "OK"
+            });
+        }
+    });
+}
+
 $(document).ready(function () {
     $('#hint-add-button').click(function (e) {
         $('#hint-edit-modal form').find("input, textarea").val("");
+
+        // Markdown Preview
+        $('#new-hint-edit').on('shown.bs.tab', function (event) {
+            console.log(event.target.hash);
+            if (event.target.hash == '#hint-preview') {
+                console.log(event.target.hash);
+                var renderer = window.markdownit({
+                    html: true,
+                });
+                var editor_value = $('#hint-write textarea').val();
+                $(event.target.hash).html(renderer.render(editor_value));
+            }
+        });
+
         $('#hint-edit-modal').modal();
     });
 
@@ -26,7 +75,7 @@ $(document).ready(function () {
         e.preventDefault();
         var hint_id = $(this).attr('hint-id');
 
-        fetch(script_root + '/api/v1/hints/' + hint_id, {
+        fetch(script_root + '/api/v1/hints/' + hint_id + '?preview=true', {
             method: 'GET',
             credentials: 'same-origin',
             headers: {
@@ -41,6 +90,19 @@ $(document).ready(function () {
                 $('#hint-edit-form input[name=cost]').val(response.data.cost);
                 $('#hint-edit-form input[name=id]').val(response.data.id);
 
+                // Markdown Preview
+                $('#new-hint-edit').on('shown.bs.tab', function (event) {
+                    console.log(event.target.hash);
+                    if (event.target.hash == '#hint-preview') {
+                        console.log(event.target.hash);
+                        var renderer = new markdownit({
+                            html: true,
+                        });
+                        var editor_value = $('#hint-write textarea').val();
+                        $(event.target.hash).html(renderer.render(editor_value));
+                    }
+                });
+
                 $('#hint-edit-modal').modal();
             }
         });
diff --git a/CTFd/themes/admin/templates/challenges/challenge.html b/CTFd/themes/admin/templates/challenges/challenge.html
index a590d942..8866ad3a 100644
--- a/CTFd/themes/admin/templates/challenges/challenge.html
+++ b/CTFd/themes/admin/templates/challenges/challenge.html
@@ -114,10 +114,12 @@
         var CHALLENGE_ID = {{ challenge.id }};
         var CHALLENGE_NAME = {{ challenge.name | tojson }};
 	</script>
+	<script src="{{ url_for('views.themes', theme='admin', path='js/vendor/markdown-it.min.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/vendor/moment.min.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/vendor/plotly.min.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/multi-modal.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/challenges/challenge.js') }}"></script>
+	<script src="{{ url_for('views.themes', theme='core', path='js/hints.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/challenges/hints.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/challenges/flags.js') }}"></script>
 	<script src="{{ url_for('views.themes', theme='admin', path='js/challenges/tags.js') }}"></script>


Test patch:
diff --git a/tests/api/v1/test_hints.py b/tests/api/v1/test_hints.py
index 4d5eadad..b21987f1 100644
--- a/tests/api/v1/test_hints.py
+++ b/tests/api/v1/test_hints.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+from CTFd.models import Hints
 from tests.helpers import *
 
 
@@ -11,6 +12,7 @@ def test_api_hint_get_non_admin():
         with login_as_user(app) as client:
             r = client.get('/api/v1/hints', json="")
             assert r.status_code == 403
+            assert Hints.query.count() == 0
     destroy_ctfd(app)
 
 
@@ -45,4 +47,44 @@ def test_api_hint_post_admin():
                 "cost": "1",
                 "challenge": 1})
             assert r.status_code == 200
+            assert Hints.query.count() == 1
+    destroy_ctfd(app)
+
+
+def test_admins_can_preview_hints():
+    """Test that admins are able to bypass restrictions and preview hints with ?preview=true"""
+    app = create_ctfd()
+    with app.app_context():
+        gen_challenge(app.db)
+        gen_hint(app.db, challenge_id=1, cost=100)
+        client = login_as_user(app, name="admin", password="password")
+        r = client.get('/api/v1/hints/1')
+        assert r.status_code == 200
+        hint = r.get_json()
+        assert hint.get('content') is None
+
+        r = client.get('/api/v1/hints/1?preview=true')
+        assert r.status_code == 200
+        hint = r.get_json()
+        assert hint['data']['content'] == "This is a hint"
+    destroy_ctfd(app)
+
+
+def test_users_cannot_preview_hints():
+    """Test that users aren't able to preview hints"""
+    app = create_ctfd()
+    with app.app_context():
+        gen_challenge(app.db)
+        gen_hint(app.db, challenge_id=1, cost=100)
+        register_user(app)
+        client = login_as_user(app)
+        r = client.get('/api/v1/hints/1')
+        assert r.status_code == 200
+        hint = r.get_json()
+        assert hint.get('content') is None
+
+        r = client.get('/api/v1/hints/1?preview=true')
+        assert r.status_code == 200
+        hint = r.get_json()
+        assert hint['data'].get('content') is None
     destroy_ctfd(app)