213 lines
6.1 KiB
Python
213 lines
6.1 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
from collections import OrderedDict
|
||
|
|
||
|
from django import forms
|
||
|
from django.urls import Resolver404, resolve
|
||
|
from django.core.validators import URLValidator
|
||
|
from django.http import QueryDict
|
||
|
|
||
|
from urllib.parse import urlparse, unquote
|
||
|
|
||
|
from .utils import Base64
|
||
|
|
||
|
|
||
|
class NextUrlField(forms.CharField):
|
||
|
"""
|
||
|
Custom ``CharField`` which validates that a value is a valid next URL.
|
||
|
|
||
|
It validates that by checking that the value can be resolved to a view
|
||
|
hence guaranteeing that when redirected URL will not fail.
|
||
|
"""
|
||
|
default_error_messages = {
|
||
|
'invalid_url': 'Invalid next url.'
|
||
|
}
|
||
|
|
||
|
def to_python(self, value):
|
||
|
"""
|
||
|
Validate that value is a valid URL for this project.
|
||
|
"""
|
||
|
value = super(NextUrlField, self).to_python(value)
|
||
|
|
||
|
if value in self.empty_values:
|
||
|
return value
|
||
|
|
||
|
path = urlparse(value).path
|
||
|
|
||
|
try:
|
||
|
resolve(path)
|
||
|
except Resolver404:
|
||
|
raise forms.ValidationError(self.error_messages['invalid_url'])
|
||
|
else:
|
||
|
return path
|
||
|
|
||
|
|
||
|
class ExtractedNextUrlField(NextUrlField):
|
||
|
"""
|
||
|
Similar to :obj:`.NextUrlField` however this extracts next url from full encoded URL.
|
||
|
"""
|
||
|
default_error_messages = {
|
||
|
'missing_next': 'Missing next query parameter.'
|
||
|
}
|
||
|
|
||
|
def to_python(self, value):
|
||
|
"""
|
||
|
Extract next url from full URL string and then use :obj:`.NextUrlField`
|
||
|
to validate that value is valid URL.
|
||
|
"""
|
||
|
value = forms.CharField.to_python(self, value)
|
||
|
|
||
|
if value in self.empty_values:
|
||
|
return value
|
||
|
|
||
|
decoded = urlparse(unquote(value))
|
||
|
data = QueryDict(decoded.query)
|
||
|
|
||
|
if 'next' not in data:
|
||
|
raise forms.ValidationError(self.error_messages['missing_next'])
|
||
|
|
||
|
return super(ExtractedNextUrlField, self).to_python(data['next'])
|
||
|
|
||
|
|
||
|
class SQRLURLValidator(URLValidator):
|
||
|
"""
|
||
|
Custom URL validator which validates that a URL is a valid SQRL url.
|
||
|
|
||
|
These are the differences with regular HTTP URLs:
|
||
|
|
||
|
* scheme is either sqrl (secure) and qrl (non-secure)
|
||
|
* ``:`` is a valid path separator which can be used to indicate
|
||
|
which section of the SQRL should be used to generate
|
||
|
public/provate keypair for the domain.
|
||
|
|
||
|
Okay, but is there support for qrl schema???
|
||
|
|
||
|
"""
|
||
|
schemes = ['sqrl', 'qrl']
|
||
|
# schemes = ['sqrl']
|
||
|
|
||
|
|
||
|
class SQRLURLField(forms.URLField):
|
||
|
"""
|
||
|
SQRL URL field which uses :obj:`.SQRLURLValidator` for validation.
|
||
|
"""
|
||
|
default_validators = [SQRLURLValidator()]
|
||
|
|
||
|
|
||
|
class Base64Field(forms.CharField):
|
||
|
"""
|
||
|
Field which decodes base64 values using :meth:`.utils.Base64.decode`.
|
||
|
"""
|
||
|
default_error_messages = {
|
||
|
'base64': 'Invalid value. Must be base64url encoded string.',
|
||
|
}
|
||
|
|
||
|
def to_python(self, value):
|
||
|
"""
|
||
|
Decodes base64 value and returns binary data.
|
||
|
"""
|
||
|
value = super(Base64Field, self).to_python(value)
|
||
|
if not value:
|
||
|
return b''
|
||
|
try:
|
||
|
return Base64.decode(value)
|
||
|
except (ValueError, TypeError):
|
||
|
raise forms.ValidationError(self.error_messages['base64'])
|
||
|
|
||
|
|
||
|
class Base64CharField(Base64Field):
|
||
|
"""
|
||
|
Similar to :obj:`.Base64Field` however this field normalizes to ``str`` (``unicode``) data.
|
||
|
"""
|
||
|
default_error_messages = {
|
||
|
'base64_ascii': 'Invalid value. Must be ascii base64url encoded string.',
|
||
|
}
|
||
|
|
||
|
def to_python(self, value):
|
||
|
"""
|
||
|
Returns base64 decoded data as string.
|
||
|
|
||
|
Uses :meth:`.Base64Field.to_python` to decode base64 value
|
||
|
which returns binary data and then this method further
|
||
|
decodes ascii data to return ``str`` (``unicode``) data.
|
||
|
"""
|
||
|
value = super(Base64CharField, self).to_python(value)
|
||
|
if not value:
|
||
|
return ''
|
||
|
try:
|
||
|
return value.decode('ascii')
|
||
|
except UnicodeDecodeError:
|
||
|
raise forms.ValidationError(self.error_messages['base64_ascii'])
|
||
|
|
||
|
|
||
|
class Base64PairsField(Base64CharField):
|
||
|
"""
|
||
|
Field which normalizes base64 encoded multistring key-value pairs to ``OrderedDict``.
|
||
|
|
||
|
Attributes
|
||
|
----------
|
||
|
always_pairs : bool
|
||
|
Boolean which enforces that the value must always be keypairs.
|
||
|
When ``False`` and the value is not a keypair, the value itself
|
||
|
is returned.
|
||
|
"""
|
||
|
default_error_messages = {
|
||
|
'crlf': 'Invalid value. Must be multi-line string separated by CRLF.',
|
||
|
'pairs': 'Invalid value. Must be multi-line string of pair of values.',
|
||
|
}
|
||
|
always_pairs = True
|
||
|
|
||
|
def to_python(self, value):
|
||
|
"""
|
||
|
Normalizes multiline base64 keypairs string to ``OrderedDict``.
|
||
|
"""
|
||
|
value = super(Base64PairsField, self).to_python(value)
|
||
|
if not value:
|
||
|
return OrderedDict()
|
||
|
|
||
|
if not value.endswith('\r\n'):
|
||
|
if self.always_pairs:
|
||
|
raise forms.ValidationError(self.error_messages['crlf'])
|
||
|
else:
|
||
|
return value
|
||
|
|
||
|
try:
|
||
|
return OrderedDict(
|
||
|
line.split('=', 1) for line in filter(None, value.splitlines())
|
||
|
)
|
||
|
except ValueError:
|
||
|
raise forms.ValidationError(self.error_messages['pairs'])
|
||
|
|
||
|
|
||
|
class Base64ConditionalPairsField(Base64PairsField):
|
||
|
"""
|
||
|
Similar to :obj:`.Base64PairsField` but this field does not force
|
||
|
the value to be keypairs.
|
||
|
"""
|
||
|
always_pairs = False
|
||
|
|
||
|
|
||
|
class TildeMultipleValuesField(forms.CharField):
|
||
|
"""
|
||
|
Field which returns tilde-separated list.
|
||
|
"""
|
||
|
|
||
|
def to_python(self, value):
|
||
|
"""
|
||
|
Normalizes to a Python list by splitting string by tilde (~) delimiter.
|
||
|
"""
|
||
|
value = super(TildeMultipleValuesField, self).to_python(value)
|
||
|
if not value:
|
||
|
return []
|
||
|
return value.split('~')
|
||
|
|
||
|
|
||
|
class TildeMultipleValuesFieldChoiceField(TildeMultipleValuesField, forms.ChoiceField):
|
||
|
"""
|
||
|
Similar to :obj:`.TildeMultipleValuesField` however this field also validates
|
||
|
each value to be a valid choice.
|
||
|
"""
|
||
|
|
||
|
def validate(self, value):
|
||
|
for i in value:
|
||
|
super(TildeMultipleValuesFieldChoiceField, self).validate(i)
|