From e83b8ac21876d7a72eea29f5b509aa467be5c0f9 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 5 Feb 2020 18:09:27 +0100 Subject: [PATCH] Allow to hide payment methods behind a secret link --- src/pretix/base/payment.py | 42 ++++++++++++++++++++++++++++-- src/pretix/presale/urls.py | 2 ++ src/pretix/presale/views/user.py | 11 ++++++++ src/tests/presale/test_checkout.py | 39 +++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 7bfe692e2e..4208dc8861 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -1,3 +1,4 @@ +import hashlib import json import logging from collections import OrderedDict @@ -14,6 +15,7 @@ from django.dispatch import receiver from django.forms import Form from django.http import HttpRequest from django.template.loader import get_template +from django.utils.crypto import get_random_string from django.utils.timezone import now from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django_countries import Countries @@ -32,7 +34,7 @@ from pretix.base.signals import register_payment_providers from pretix.base.templatetags.money import money_filter from pretix.base.templatetags.rich_text import rich_text from pretix.helpers.money import DecimalTextInput -from pretix.multidomain.urlreverse import eventreverse +from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.presale.views import get_cart, get_cart_total from pretix.presale.views.cart import cart_session, get_or_create_cart_id @@ -204,6 +206,13 @@ class BasePaymentProvider: implementation. """ places = settings.CURRENCY_PLACES.get(self.event.currency, 2) + + if not self.settings.get('_hidden_seed'): + self.settings.set('_hidden_seed', get_random_string(64)) + hidden_url = build_absolute_uri(self.event, 'presale:event.payment.unlock', kwargs={ + 'hash': hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest(), + }) + d = OrderedDict([ ('_enabled', forms.BooleanField( @@ -297,7 +306,28 @@ class BasePaymentProvider: widget=forms.CheckboxSelectMultiple, help_text=_( 'Only allow the usage of this payment provider in the following sales channels'), - )) + )), + ('_hidden', + forms.BooleanField( + label=_('Hide payment method'), + help_text=_( + 'The payment method will not be shown by default but only to people who enter the shop through ' + 'a special link.' + ), + )), + ('_hidden_url', + forms.URLField( + label=_('Link to enable payment method'), + widget=forms.TextInput(attrs={ + 'readonly': 'readonly', + 'data-display-dependency': '#id_%s_hidden' % self.settings.get_prefix(), + 'value': hidden_url, + }), + initial=hidden_url, + help_text=_( + 'Share this link with customers who should use this payment method.' + ), + )), ]) d['_restricted_countries']._as_type = list d['_restrict_to_sales_channels']._as_type = list @@ -433,6 +463,11 @@ class BasePaymentProvider: if self.settings._total_min is not None: pricing = pricing and total >= Decimal(self.settings._total_min) + if self.settings._hidden: + hashes = request.session.get('pretix_unlock_hashes', []) + if hashlib.sha256((self.settings._hidden_seed + self.event.slug).encode()).hexdigest() not in hashes: + return False + def get_invoice_address(): if not hasattr(request, '_checkout_flow_invoice_address'): cs = cart_session(request) @@ -602,6 +637,9 @@ class BasePaymentProvider: if self.settings._total_min is not None and ps < Decimal(self.settings._total_min): return False + if self.settings._hidden: + return False + restricted_countries = self.settings.get('_restricted_countries', as_type=list) if restricted_countries: try: diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py index 69ccb4c199..8ba461afa2 100644 --- a/src/pretix/presale/urls.py +++ b/src/pretix/presale/urls.py @@ -53,6 +53,8 @@ event_patterns = [ csrf_exempt(pretix.presale.views.cart.CartAdd.as_view()), name='event.cart.add'), + url(r'unlock/(?P[a-z0-9]{64})/$', pretix.presale.views.user.UnlockHashView.as_view(), + name='event.payment.unlock'), url(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'), url(r'^order/(?P[^/]+)/(?P[A-Za-z0-9]+)/open/(?P[a-z0-9]+)/$', pretix.presale.views.order.OrderOpen.as_view(), diff --git a/src/pretix/presale/views/user.py b/src/pretix/presale/views/user.py index 9968bbb13d..12103e55cd 100644 --- a/src/pretix/presale/views/user.py +++ b/src/pretix/presale/views/user.py @@ -5,6 +5,7 @@ from django.contrib import messages from django.shortcuts import redirect from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from django.views import View from django.views.generic import TemplateView from pretix.base.email import get_email_context @@ -60,3 +61,13 @@ class ResendLinkView(EventViewMixin, TemplateView): context = super().get_context_data(**kwargs) context['form'] = self.link_form return context + + +class UnlockHashView(EventViewMixin, View): + # Allows to register an unlock hash in the user's session, e.g. to unlock a hidden payment provider + + def get(self, request, *args, **kwargs): + hashes = request.session.get('pretix_unlock_hashes', []) + hashes.append(kwargs.get('hash')) + request.session['pretix_unlock_hashes'] = hashes + return redirect(eventreverse(self.request.event, 'presale:event.index')) diff --git a/src/tests/presale/test_checkout.py b/src/tests/presale/test_checkout.py index 03091ef7db..c869da1bac 100644 --- a/src/tests/presale/test_checkout.py +++ b/src/tests/presale/test_checkout.py @@ -1,4 +1,5 @@ import datetime +import hashlib import json import os from datetime import timedelta @@ -9,6 +10,7 @@ from bs4 import BeautifulSoup from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase +from django.utils.crypto import get_random_string from django.utils.timezone import now from django_countries.fields import Country from django_scopes import scopes_disabled @@ -701,6 +703,43 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase): doc = BeautifulSoup(response.rendered_content, "lxml") assert doc.select(".alert-danger") + def test_payment_hidden(self): + self.event.settings.set('payment_stripe__enabled', True) + self.event.settings.set('payment_banktransfer__enabled', True) + self.event.settings.set('payment_banktransfer__hidden', True) + self.event.settings.set('payment_banktransfer__hidden_seed', get_random_string(32)) + with scopes_disabled(): + CartPosition.objects.create( + event=self.event, cart_id=self.session_key, item=self.ticket, + price=23, expires=now() + timedelta(minutes=10) + ) + response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select('input[name="payment"]')), 1) + response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), { + 'payment': 'banktransfer' + }, follow=True) + self.assertEqual(response.status_code, 200) + doc = BeautifulSoup(response.rendered_content, "lxml") + assert doc.select(".alert-danger") + + self.client.get('/%s/%s/unlock/%s/' % ( + self.orga.slug, self.event.slug, + hashlib.sha256( + (self.event.settings.payment_banktransfer__hidden_seed + self.event.slug).encode() + ).hexdigest(), + ), follow=True) + + response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True) + doc = BeautifulSoup(response.rendered_content, "lxml") + self.assertEqual(len(doc.select('input[name="payment"]')), 2) + response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), { + 'payment': 'banktransfer' + }, follow=True) + self.assertEqual(response.status_code, 200) + doc = BeautifulSoup(response.rendered_content, "lxml") + assert not doc.select(".alert-danger") + def test_payment_min_value(self): self.event.settings.set('payment_stripe__enabled', True) self.event.settings.set('payment_banktransfer__total_min', Decimal('42.00'))