mirror of
https://github.com/pretix/pretix.git
synced 2025-12-07 22:42:26 +00:00
Compare commits
2 Commits
rename-con
...
v4.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb31d899c2 | ||
|
|
03a60fb561 |
@@ -611,8 +611,12 @@ Order position endpoints
|
|||||||
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
|
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
|
||||||
accepts a number of optional requests in the body.
|
accepts a number of optional requests in the body.
|
||||||
|
|
||||||
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter.
|
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter. In this case, you should
|
||||||
|
always set ``untrusted_input=true`` as a query parameter to avoid security issues.
|
||||||
|
|
||||||
|
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
||||||
|
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
||||||
|
data to avoid guessing of ticket IDs.
|
||||||
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
|
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
|
||||||
you do not implement question handling in your user interface, you **must**
|
you do not implement question handling in your user interface, you **must**
|
||||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "4.10.0"
|
__version__ = "4.10.1"
|
||||||
|
|||||||
@@ -409,6 +409,11 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
||||||
nonce = self.request.data.get('nonce')
|
nonce = self.request.data.get('nonce')
|
||||||
|
|
||||||
|
untrusted_input = (
|
||||||
|
self.request.GET.get('untrusted_input', '') not in ('0', 'false', 'False', '')
|
||||||
|
or (isinstance(self.request.auth, Device) and 'pretixscan' in (self.request.auth.software_brand or '').lower())
|
||||||
|
)
|
||||||
|
|
||||||
if 'datetime' in self.request.data:
|
if 'datetime' in self.request.data:
|
||||||
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
|
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
|
||||||
else:
|
else:
|
||||||
@@ -429,7 +434,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||||
if self.kwargs['pk'].isnumeric():
|
if self.kwargs['pk'].isnumeric() and not untrusted_input:
|
||||||
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||||
else:
|
else:
|
||||||
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false)">
|
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false, true)">
|
||||||
{{ $root.strings['modal.continue'] }}
|
{{ $root.strings['modal.continue'] }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-default" @click="showUnpaidModal = false">
|
<button type="button" class="btn btn-default" @click="showUnpaidModal = false">
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true)">
|
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true, true)">
|
||||||
{{ $root.strings['modal.continue'] }}
|
{{ $root.strings['modal.continue'] }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-default" @click="showQuestionsModal = false">
|
<button type="button" class="btn btn-default" @click="showQuestionsModal = false">
|
||||||
@@ -296,7 +296,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectResult(res) {
|
selectResult(res) {
|
||||||
this.check(res.id, false, false, false)
|
this.check(res.id, false, false, false, false)
|
||||||
},
|
},
|
||||||
answerSetM(qid, opid, checked) {
|
answerSetM(qid, opid, checked) {
|
||||||
let arr = this.answers[qid] ? this.answers[qid].split(',') : [];
|
let arr = this.answers[qid] ? this.answers[qid].split(',') : [];
|
||||||
@@ -320,7 +320,7 @@ export default {
|
|||||||
this.showQuestionsModal = false
|
this.showQuestionsModal = false
|
||||||
this.answers = {}
|
this.answers = {}
|
||||||
},
|
},
|
||||||
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch) {
|
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch, untrusted) {
|
||||||
if (!keepAnswers) {
|
if (!keepAnswers) {
|
||||||
this.answers = {}
|
this.answers = {}
|
||||||
} else if (this.showQuestionsModal) {
|
} else if (this.showQuestionsModal) {
|
||||||
@@ -339,7 +339,11 @@ export default {
|
|||||||
this.$refs.input.blur()
|
this.$refs.input.blur()
|
||||||
})
|
})
|
||||||
|
|
||||||
fetch(this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation', {
|
let url = this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation'
|
||||||
|
if (untrusted) {
|
||||||
|
url += '&untrusted_input=true'
|
||||||
|
}
|
||||||
|
fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': document.querySelector("input[name=csrfmiddlewaretoken]").value,
|
'X-CSRFToken': document.querySelector("input[name=csrfmiddlewaretoken]").value,
|
||||||
@@ -439,7 +443,7 @@ export default {
|
|||||||
startSearch(fallbackToScan) {
|
startSearch(fallbackToScan) {
|
||||||
if (this.query.length >= 32 && fallbackToScan) {
|
if (this.query.length >= 32 && fallbackToScan) {
|
||||||
// likely a secret, not a search result
|
// likely a secret, not a search result
|
||||||
this.check(this.query, false, false, true)
|
this.check(this.query, false, false, true, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1199,7 +1199,6 @@ def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clis
|
|||||||
), {
|
), {
|
||||||
'force': True
|
'force': True
|
||||||
}, format='json')
|
}, format='json')
|
||||||
print(resp.data)
|
|
||||||
assert resp.status_code == 400
|
assert resp.status_code == 400
|
||||||
assert resp.data["status"] == "error"
|
assert resp.data["status"] == "error"
|
||||||
assert resp.data["reason"] == "already_redeemed"
|
assert resp.data["reason"] == "already_redeemed"
|
||||||
@@ -1219,3 +1218,43 @@ def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clis
|
|||||||
assert resp.data["reason"] == "invalid"
|
assert resp.data["reason"] == "invalid"
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
assert not Checkin.objects.last()
|
assert not Checkin.objects.last()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_redeem_by_id_not_allowed_if_pretixscan(device, device_client, organizer, clist, event, order):
|
||||||
|
with scopes_disabled():
|
||||||
|
p = order.positions.first()
|
||||||
|
device.software_brand = "pretixSCAN"
|
||||||
|
device.software_version = "1.14.2"
|
||||||
|
device.save()
|
||||||
|
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||||
|
organizer.slug, event.slug, clist.pk, p.pk
|
||||||
|
), {
|
||||||
|
'force': True
|
||||||
|
}, format='json')
|
||||||
|
print(resp.data)
|
||||||
|
assert resp.status_code == 404
|
||||||
|
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||||
|
organizer.slug, event.slug, clist.pk, p.secret
|
||||||
|
), {
|
||||||
|
'force': True
|
||||||
|
}, format='json')
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_redeem_by_id_not_allowed_if_untrusted(device, device_client, organizer, clist, event, order):
|
||||||
|
with scopes_disabled():
|
||||||
|
p = order.positions.first()
|
||||||
|
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||||
|
organizer.slug, event.slug, clist.pk, p.pk
|
||||||
|
), {
|
||||||
|
'force': True
|
||||||
|
}, format='json')
|
||||||
|
assert resp.status_code == 404
|
||||||
|
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||||
|
organizer.slug, event.slug, clist.pk, p.secret
|
||||||
|
), {
|
||||||
|
'force': True
|
||||||
|
}, format='json')
|
||||||
|
assert resp.status_code == 201
|
||||||
|
|||||||
Reference in New Issue
Block a user